From ccc0b51a999a6e3834d41620b827546875c784e1 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 01:53:32 +0200 Subject: [PATCH 1/6] Resource Tree: Improve mtrl and sklb support --- .../Interop/ResourceTree/ResolveContext.cs | 107 ++++++++++++++---- Penumbra/Interop/ResourceTree/ResourceNode.cs | 28 +++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 30 +++-- .../Interop/ResourceTree/TreeBuildCache.cs | 5 - .../UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- 5 files changed, 129 insertions(+), 43 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 0cb854f3..8a27f02b 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Collections; using Penumbra.GameData; @@ -23,17 +24,17 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private ResourceNode? CreateNodeFromShpk(nint sourceAddress, ByteString gamePath, bool @internal) + private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) { if (gamePath.IsEmpty) return null; if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) return null; - return CreateNodeFromGamePath(ResourceType.Shpk, sourceAddress, path, @internal); + return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->Handle, path, @internal); } - private ResourceNode? CreateNodeFromTex(nint sourceAddress, ByteString gamePath, bool @internal, bool dx11) + private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11) { if (dx11) { @@ -59,13 +60,19 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (!Utf8GamePath.FromByteString(gamePath, out var path)) return null; - return CreateNodeFromGamePath(ResourceType.Tex, sourceAddress, path, @internal); + return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal); } - private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal) - => new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal); + private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath, bool @internal) + { + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; + if (fullPath.InternalName.IsEmpty) + fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); + + return new(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + } - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, bool withName) { var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; @@ -79,13 +86,14 @@ private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddres gamePaths = Filter(gamePaths); if (gamePaths.Count == 1) - return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, sourceAddress, gamePaths[0], fullPath, @internal); + return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, objectAddress, (nint)handle, gamePaths[0], fullPath, + GetResourceHandleLength(handle), @internal); Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); foreach (var gamePath in gamePaths) Penumbra.Log.Information($"Game path: {gamePath}"); - return new ResourceNode(null, type, sourceAddress, gamePaths.ToArray(), fullPath, @internal); + return new ResourceNode(null, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); } public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) { @@ -95,12 +103,12 @@ private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddres if (!Utf8GamePath.FromString(path, out var gamePath)) return null; - return CreateNodeFromGamePath(ResourceType.Sklb, 0, gamePath, false); + return CreateNodeFromGamePath(ResourceType.Sklb, 0, null, gamePath, false); } public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { - var node = CreateNodeFromResourceHandle(ResourceType.Imc, (nint) imc, imc, true, false); + var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false); if (node == null) return null; @@ -113,8 +121,8 @@ private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddres return node; } - public unsafe ResourceNode? CreateNodeFromTex(ResourceHandle* tex) - => CreateNodeFromResourceHandle(ResourceType.Tex, (nint) tex, tex, false, WithNames); + public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) + => CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl) { @@ -145,6 +153,38 @@ private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddres private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl) { + static ushort GetTextureIndex(ushort texFlags) + { + if ((texFlags & 0x001F) != 0x001F) + return (ushort)(texFlags & 0x001F); + else if ((texFlags & 0x03E0) != 0x03E0) + return (ushort)((texFlags >> 5) & 0x001F); + else + return (ushort)((texFlags >> 10) & 0x001F); + } + static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle) + { + var textures = mtrl->Textures; + for (var i = 0; i < mtrl->TextureCount; ++i) + { + if (textures[i].ResourceHandle == handle) + return textures[i].Id; + } + + return null; + } + static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) + { + var samplers = (ShaderPackageUtility.Sampler*)shpk->Samplers; + for (var i = 0; i < shpk->SamplerCount; ++i) + { + if (samplers[i].Id == id) + return samplers[i].Crc; + } + + return null; + } + if (mtrl == null) return null; @@ -153,23 +193,36 @@ private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddres if (node == null) return null; - var mtrlFile = WithNames ? TreeBuildCache.ReadMaterial(node.FullPath) : null; - - var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false); + var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); if (shpkNode != null) node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null; + var shpk = WithNames && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; for (var i = 0; i < resource->NumTex; i++) { - var texNode = CreateNodeFromTex(nint.Zero, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i)); + var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i)); if (texNode == null) continue; if (WithNames) { - var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null; + string? name = null; + if (shpk != null) + { + var index = GetTextureIndex(resource->TexSpace[i].Flags); + uint? samplerId; + if (index != 0x001F) + samplerId = mtrl->Textures[index].Id; + else + samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle); + if (samplerId.HasValue) + { + var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); + if (samplerCrc.HasValue) + name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; + } + } node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); } else @@ -181,6 +234,14 @@ private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddres return node; } + public unsafe ResourceNode? CreateNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + { + if (sklb->SkeletonResourceHandle == null) + return null; + + return CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + } + private FullPath FilterFullPath(FullPath fullPath) { if (!fullPath.IsRooted) @@ -294,4 +355,12 @@ internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle) return name; } + + static unsafe ulong GetResourceHandleLength(ResourceHandle* handle) + { + if (handle == null) + return 0; + + return ResourceHandle.GetLength(handle); + } } diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index dc0c5fcb..bceda36c 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -9,37 +9,43 @@ public class ResourceNode { public readonly string? Name; public readonly ResourceType Type; - public readonly nint SourceAddress; + public readonly nint ObjectAddress; + public readonly nint ResourceHandle; public readonly Utf8GamePath GamePath; public readonly Utf8GamePath[] PossibleGamePaths; public readonly FullPath FullPath; + public readonly ulong Length; public readonly bool Internal; public readonly List Children; - public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath gamePath, FullPath fullPath, bool @internal) + public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, ulong length, bool @internal) { - Name = name; - Type = type; - SourceAddress = sourceAddress; - GamePath = gamePath; + Name = name; + Type = type; + ObjectAddress = objectAddress; + ResourceHandle = resourceHandle; + GamePath = gamePath; PossibleGamePaths = new[] { gamePath, }; FullPath = fullPath; + Length = length; Internal = @internal; Children = new List(); } - public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath[] possibleGamePaths, FullPath fullPath, - bool @internal) + public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, + ulong length, bool @internal) { Name = name; Type = type; - SourceAddress = sourceAddress; + ObjectAddress = objectAddress; + ResourceHandle = resourceHandle; GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty; PossibleGamePaths = possibleGamePaths; FullPath = fullPath; + Length = length; Internal = @internal; Children = new List(); } @@ -48,10 +54,12 @@ private ResourceNode(string? name, ResourceNode originalResourceNode) { Name = name; Type = originalResourceNode.Type; - SourceAddress = originalResourceNode.SourceAddress; + ObjectAddress = originalResourceNode.ObjectAddress; + ResourceHandle = originalResourceNode.ResourceHandle; GamePath = originalResourceNode.GamePath; PossibleGamePaths = originalResourceNode.PossibleGamePaths; FullPath = originalResourceNode.FullPath; + Length = originalResourceNode.Length; Internal = originalResourceNode.Internal; Children = originalResourceNode.Children; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 76d0c3f2..f14191c8 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -57,7 +58,9 @@ internal unsafe void LoadResources(GlobalResolveContext globalContext) var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); - } + } + + AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc) AddHumanResources(globalContext, (HumanExt*)model); @@ -95,7 +98,9 @@ private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanE subObjectNodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}") : mdlNode); - } + } + + AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; ++subObjectIndex; @@ -106,16 +111,25 @@ private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanE var context = globalContext.CreateContext(EquipSlot.Unknown, default); - var skeletonNode = context.CreateHumanSkeletonNode((GenderRace)human->Human.RaceSexId); - if (skeletonNode != null) - Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode); - - var decalNode = context.CreateNodeFromTex(human->Decal); + var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) Nodes.Add(globalContext.WithNames ? decalNode.WithName(decalNode.Name ?? "Face Decal") : decalNode); - var legacyDecalNode = context.CreateNodeFromTex(human->LegacyBodyDecal); + var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) Nodes.Add(globalContext.WithNames ? legacyDecalNode.WithName(legacyDecalNode.Name ?? "Legacy Body Decal") : legacyDecalNode); + } + + private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") + { + if (skeleton == null) + return; + + for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) + { + var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); + if (sklbNode != null) + nodes.Add(context.WithNames ? sklbNode.WithName($"{prefix}Skeleton #{i}") : sklbNode); + } } } diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index e9939496..d29916dd 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -12,7 +12,6 @@ namespace Penumbra.Interop.ResourceTree; internal class TreeBuildCache { private readonly IDataManager _dataManager; - private readonly Dictionary _materials = new(); private readonly Dictionary _shaderPackages = new(); public readonly List Characters; public readonly Dictionary CharactersById; @@ -27,10 +26,6 @@ public TreeBuildCache(IObjectTable objects, IDataManager dataManager) .ToDictionary(c => c.Key, c => c.First()); } - /// Try to read a material file from the given path and cache it on success. - public MtrlFile? ReadMaterial(FullPath path) - => ReadFile(_dataManager, path, _materials, bytes => new MtrlFile(bytes)); - /// Try to read a shpk file from the given path and cache it on success. public ShpkFile? ReadShaderPackage(FullPath path) => ReadFile(_dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 4d8c77a7..0d87215f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -128,7 +128,7 @@ private void DrawNodes(IEnumerable resourceNodes, int level) if (debugMode) ImGuiUtil.HoverTooltip( - $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress:X16}"); + $"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X}"); } ImGui.TableNextColumn(); From db521dd21cc8438308c2e2d983d7fbfb704211f0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 04:52:54 +0200 Subject: [PATCH 2/6] Resource Tree: Deduplicate nodes, add skp --- .../Interop/ResourceTree/ResolveContext.cs | 76 +++++++++++++++++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 34 +++++---- .../ResourceTree/ResourceTreeFactory.cs | 6 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 32 +++++--- 4 files changed, 114 insertions(+), 34 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 8a27f02b..a97ff726 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -11,21 +11,27 @@ using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.Interop.ResourceTree; internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames) { + public readonly Dictionary Nodes = new(128); + public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, slot, equipment); + => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, Nodes, slot, equipment); } -internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot, - CharacterArmor Equipment) +internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, + Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) { + if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) + return cached; + if (gamePath.IsEmpty) return null; if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) @@ -36,6 +42,9 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11) { + if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) + return cached; + if (dx11) { var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/'); @@ -69,7 +78,11 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (fullPath.InternalName.IsEmpty) fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - return new(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + var node = new ResourceNode(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + if (resourceHandle != null) + Nodes.Add((nint)resourceHandle, node); + + return node; } private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, @@ -95,6 +108,7 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec return new ResourceNode(null, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); } + public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) { var raceSexIdStr = gr.ToRaceCode(); @@ -108,6 +122,9 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { + if (Nodes.TryGetValue((nint)imc, out var cached)) + return cached; + var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false); if (node == null) return null; @@ -118,17 +135,31 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec node = node.WithName(name != null ? $"IMC: {name}" : null); } + Nodes.Add((nint)imc, node); + return node; } public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) - => CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); + { + if (Nodes.TryGetValue((nint)tex, out var cached)) + return cached; + + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); + if (node != null) + Nodes.Add((nint)tex, node); + + return node; + } public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl) { if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) return null; + if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached)) + return cached; + var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint) mdl, mdl->ResourceHandle, false, false); if (node == null) return null; @@ -148,6 +179,8 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec : mtrlNode); } + Nodes.Add((nint)mdl->ResourceHandle, node); + return node; } @@ -188,7 +221,10 @@ static ushort GetTextureIndex(ushort texFlags) if (mtrl == null) return null; - var resource = mtrl->ResourceHandle; + var resource = mtrl->ResourceHandle; + if (Nodes.TryGetValue((nint)resource, out var cached)) + return cached; + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); if (node == null) return null; @@ -231,6 +267,8 @@ static ushort GetTextureIndex(ushort texFlags) } } + Nodes.Add((nint)resource, node); + return node; } @@ -239,7 +277,29 @@ static ushort GetTextureIndex(ushort texFlags) if (sklb->SkeletonResourceHandle == null) return null; - return CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) + return cached; + + var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + if (node != null) + Nodes.Add((nint)sklb->SkeletonResourceHandle, node); + + return node; + } + + public unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + { + if (sklb->SkeletonParameterResourceHandle == null) + return null; + + if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) + return cached; + + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithNames); + if (node != null) + Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); + + return node; } private FullPath FilterFullPath(FullPath fullPath) @@ -356,7 +416,7 @@ internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle) return name; } - static unsafe ulong GetResourceHandleLength(ResourceHandle* handle) + private static unsafe ulong GetResourceHandleLength(ResourceHandle* handle) { if (handle == null) return 0; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index f14191c8..f3e6ca51 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -13,29 +13,33 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceTree { - public readonly string Name; - public readonly nint SourceAddress; - public readonly bool PlayerRelated; - public readonly string CollectionName; - public readonly List Nodes; + public readonly string Name; + public readonly nint GameObjectAddress; + public readonly nint DrawObjectAddress; + public readonly bool PlayerRelated; + public readonly string CollectionName; + public readonly List Nodes; + public readonly HashSet FlatNodes; public int ModelId; public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, nint sourceAddress, bool playerRelated, string collectionName) + public ResourceTree(string name, nint gameObjectAddress, nint drawObjectAddress, bool playerRelated, string collectionName) { - Name = name; - SourceAddress = sourceAddress; - PlayerRelated = playerRelated; - CollectionName = collectionName; - Nodes = new List(); + Name = name; + GameObjectAddress = gameObjectAddress; + DrawObjectAddress = drawObjectAddress; + PlayerRelated = playerRelated; + CollectionName = collectionName; + Nodes = new List(); + FlatNodes = new HashSet(); } internal unsafe void LoadResources(GlobalResolveContext globalContext) { - var character = (Character*)SourceAddress; - var model = (CharacterBase*)character->GameObject.GetDrawObject(); + var character = (Character*)GameObjectAddress; + var model = (CharacterBase*)DrawObjectAddress; var equipment = new ReadOnlySpan(&character->DrawData.Head, 10); // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); ModelId = character->CharacterData.ModelCharaId; @@ -130,6 +134,10 @@ private unsafe void AddSkeleton(List nodes, ResolveContext context var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) nodes.Add(context.WithNames ? sklbNode.WithName($"{prefix}Skeleton #{i}") : sklbNode); + + var skpNode = context.CreateParameterNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); + if (skpNode != null) + nodes.Add(context.WithNames ? skpNode.WithName($"{prefix}Skeleton #{i} Parameters") : skpNode); } } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 98c1b305..f416dc12 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -62,7 +62,8 @@ public ResourceTree[] FromObjectTable(bool withNames = true) return null; var gameObjStruct = (GameObject*)character.Address; - if (gameObjStruct->GetDrawObject() == null) + var drawObjStruct = gameObjStruct->GetDrawObject(); + if (drawObjStruct == null) return null; var collectionResolveData = _collectionResolver.IdentifyCollection(gameObjStruct, true); @@ -70,10 +71,11 @@ public ResourceTree[] FromObjectTable(bool withNames = true) return null; var (name, related) = GetCharacterName(character, cache); - var tree = new ResourceTree(name, (nint)gameObjStruct, related, collectionResolveData.ModCollection.Name); + var tree = new ResourceTree(name, (nint)gameObjStruct, (nint)drawObjStruct, related, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withNames); tree.LoadResources(globalContext); + tree.FlatNodes.UnionWith(globalContext.Nodes.Values); return tree; } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 0d87215f..8e996eb7 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -18,7 +18,7 @@ public class ResourceTreeViewer private readonly int _actionCapacity; private readonly Action _onRefresh; private readonly Action _drawActions; - private readonly HashSet _unfolded; + private readonly HashSet _unfolded; private Task? _task; @@ -30,7 +30,7 @@ public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, _actionCapacity = actionCapacity; _onRefresh = onRefresh; _drawActions = drawActions; - _unfolded = new HashSet(); + _unfolded = new HashSet(); } public void Draw() @@ -82,7 +82,7 @@ public void Draw() (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - DrawNodes(tree.Nodes, 0); + DrawNodes(tree.Nodes, 0, 0); } } } @@ -101,7 +101,7 @@ private Task RefreshCharacterList() } }); - private void DrawNodes(IEnumerable resourceNodes, int level) + private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash) { var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); @@ -109,26 +109,36 @@ private void DrawNodes(IEnumerable resourceNodes, int level) foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { if (resourceNode.Internal && !debugMode) - continue; + continue; + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorInternal = (textColor & 0x00FFFFFFu) | ((textColor & 0xFE000000u) >> 1); // Half opacity + + using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); + + var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); - var unfolded = _unfolded.Contains(resourceNode); + var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) { if (unfolded) - _unfolded.Remove(resourceNode); + _unfolded.Remove(nodePathHash); else - _unfolded.Add(resourceNode); + _unfolded.Add(nodePathHash); unfolded = !unfolded; } - if (debugMode) + if (debugMode) + { + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); ImGuiUtil.HoverTooltip( - $"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X}"); + $"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X16}"); + } } ImGui.TableNextColumn(); @@ -171,7 +181,7 @@ private void DrawNodes(IEnumerable resourceNodes, int level) } if (unfolded) - DrawNodes(resourceNode.Children, level + 1); + DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31)); } } } From 30c622c085d658ca69bf980a707ab1860b9993f8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 06:06:42 +0200 Subject: [PATCH 3/6] Resource Tree: Add ChangedItem-like icons, make UI prettier --- .../Interop/ResourceTree/ResolveContext.cs | 72 ++++++++++--------- Penumbra/Interop/ResourceTree/ResourceNode.cs | 31 +++++--- Penumbra/Interop/ResourceTree/ResourceTree.cs | 20 +++--- .../ResourceTree/ResourceTreeFactory.cs | 17 ++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 5 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 44 ++++++++---- Penumbra/UI/ChangedItemDrawer.cs | 41 ++++++----- Penumbra/UI/Tabs/OnScreenTab.cs | 4 +- 8 files changed, 139 insertions(+), 95 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index a97ff726..72a5be69 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -11,20 +11,21 @@ using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; -using static Penumbra.GameData.Files.ShpkFile; +using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; -internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames) +internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, + bool RedactExternalPaths) { public readonly Dictionary Nodes = new(128); public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, Nodes, slot, equipment); + => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithUIData, RedactExternalPaths, Nodes, slot, equipment); } -internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, - Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, + bool RedactExternalPaths, Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) @@ -78,7 +79,7 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (fullPath.InternalName.IsEmpty) fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - var node = new ResourceNode(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); if (resourceHandle != null) Nodes.Add((nint)resourceHandle, node); @@ -99,14 +100,14 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec gamePaths = Filter(gamePaths); if (gamePaths.Count == 1) - return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, objectAddress, (nint)handle, gamePaths[0], fullPath, + return new ResourceNode(withName ? GuessUIDataFromPath(gamePaths[0]) : default, type, objectAddress, (nint)handle, gamePaths[0], fullPath, GetResourceHandleLength(handle), @internal); Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); foreach (var gamePath in gamePaths) Penumbra.Log.Information($"Game path: {gamePath}"); - return new ResourceNode(null, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); + return new ResourceNode(default, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); } public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) @@ -129,10 +130,10 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (node == null) return null; - if (WithNames) + if (WithUIData) { - var name = GuessModelName(node.GamePath); - node = node.WithName(name != null ? $"IMC: {name}" : null); + var uiData = GuessModelUIData(node.GamePath); + node = node.WithUIData(uiData.PrependName("IMC: ")); } Nodes.Add((nint)imc, node); @@ -145,7 +146,7 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithUIData); if (node != null) Nodes.Add((nint)tex, node); @@ -164,8 +165,8 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (node == null) return null; - if (WithNames) - node = node.WithName(GuessModelName(node.GamePath)); + if (WithUIData) + node = node.WithUIData(GuessModelUIData(node.GamePath)); for (var i = 0; i < mdl->MaterialCount; i++) { @@ -173,9 +174,9 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) // Don't keep the material's name if it's redundant with the model's name. - node.Children.Add(WithNames - ? mtrlNode.WithName((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) - ?? $"Material #{i}") + node.Children.Add(WithUIData + ? mtrlNode.WithUIData((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) + ?? $"Material #{i}", mtrlNode.Icon) : mtrlNode); } @@ -225,15 +226,15 @@ static ushort GetTextureIndex(ushort texFlags) if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithUIData); if (node == null) return null; var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); if (shpkNode != null) - node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); - var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var shpk = WithNames && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + node.Children.Add(WithUIData ? shpkNode.WithUIData("Shader Package", 0) : shpkNode); + var shpkFile = WithUIData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; + var shpk = WithUIData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; for (var i = 0; i < resource->NumTex; i++) { @@ -241,7 +242,7 @@ static ushort GetTextureIndex(ushort texFlags) if (texNode == null) continue; - if (WithNames) + if (WithUIData) { string? name = null; if (shpk != null) @@ -259,7 +260,7 @@ static ushort GetTextureIndex(ushort texFlags) name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; } } - node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); + node.Children.Add(texNode.WithUIData(name ?? $"Texture #{i}", 0)); } else { @@ -280,7 +281,7 @@ static ushort GetTextureIndex(ushort texFlags) if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithUIData); if (node != null) Nodes.Add((nint)sklb->SkeletonResourceHandle, node); @@ -295,7 +296,7 @@ static ushort GetTextureIndex(ushort texFlags) if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithUIData); if (node != null) Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); @@ -308,7 +309,7 @@ private FullPath FilterFullPath(FullPath fullPath) return fullPath; var relPath = Path.GetRelativePath(Config.ModDirectory, fullPath.FullName); - if (relPath == "." || !relPath.StartsWith('.') && !Path.IsPathRooted(relPath)) + if (!RedactExternalPaths || relPath == "." || !relPath.StartsWith('.') && !Path.IsPathRooted(relPath)) return fullPath.Exists ? fullPath : FullPath.Empty; return FullPath.Empty; @@ -351,7 +352,7 @@ private List Filter(List gamePaths) } : false; - private string? GuessModelName(Utf8GamePath gamePath) + private ResourceNode.UIData GuessModelUIData(Utf8GamePath gamePath) { var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); // Weapons intentionally left out. @@ -359,23 +360,24 @@ private List Filter(List gamePaths) if (isEquipment) foreach (var item in Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot())) { - return Slot switch + var name = Slot switch { EquipSlot.RFinger => "R: ", EquipSlot.LFinger => "L: ", _ => string.Empty, } + item.Name.ToString(); + return new(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); } - var nameFromPath = GuessNameFromPath(gamePath); - if (nameFromPath != null) - return nameFromPath; + var dataFromPath = GuessUIDataFromPath(gamePath); + if (dataFromPath.Name != null) + return dataFromPath; - return isEquipment ? Slot.ToName() : null; + return isEquipment ? new(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) : new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } - private string? GuessNameFromPath(Utf8GamePath gamePath) + private ResourceNode.UIData GuessUIDataFromPath(Utf8GamePath gamePath) { foreach (var obj in Identifier.Identify(gamePath.ToString())) { @@ -383,10 +385,10 @@ private List Filter(List gamePaths) if (name.StartsWith("Customization:")) name = name[14..].Trim(); if (name != "Unknown") - return name; + return new(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); } - return null; + return new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index bceda36c..17584787 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using Penumbra.GameData.Enums; using Penumbra.String.Classes; +using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; namespace Penumbra.Interop.ResourceTree; public class ResourceNode { public readonly string? Name; + public readonly ChangedItemIcon Icon; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -18,9 +20,11 @@ public class ResourceNode public readonly bool Internal; public readonly List Children; - public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, ulong length, bool @internal) + public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, + ulong length, bool @internal) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; @@ -35,10 +39,11 @@ public ResourceNode(string? name, ResourceType type, nint objectAddress, nint re Children = new List(); } - public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, + public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, ulong length, bool @internal) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; @@ -50,9 +55,10 @@ public ResourceNode(string? name, ResourceType type, nint objectAddress, nint re Children = new List(); } - private ResourceNode(string? name, ResourceNode originalResourceNode) + private ResourceNode(UIData uiData, ResourceNode originalResourceNode) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; Type = originalResourceNode.Type; ObjectAddress = originalResourceNode.ObjectAddress; ResourceHandle = originalResourceNode.ResourceHandle; @@ -64,6 +70,15 @@ private ResourceNode(string? name, ResourceNode originalResourceNode) Children = originalResourceNode.Children; } - public ResourceNode WithName(string? name) - => string.Equals(Name, name, StringComparison.Ordinal) ? this : new ResourceNode(name, this); + public ResourceNode WithUIData(string? name, ChangedItemIcon icon) + => string.Equals(Name, name, StringComparison.Ordinal) && Icon == icon ? this : new ResourceNode(new(name, icon), this); + + public ResourceNode WithUIData(UIData uiData) + => string.Equals(Name, uiData.Name, StringComparison.Ordinal) && Icon == uiData.Icon ? this : new ResourceNode(uiData, this); + + public readonly record struct UIData(string? Name, ChangedItemIcon Icon) + { + public readonly UIData PrependName(string prefix) + => Name == null ? this : new(prefix + Name, Icon); + } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index f3e6ca51..c6e90c6b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -56,12 +56,12 @@ internal unsafe void LoadResources(GlobalResolveContext globalContext) var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = context.CreateNodeFromImc(imc); if (imcNode != null) - Nodes.Add(globalContext.WithNames ? imcNode.WithName(imcNode.Name ?? $"IMC #{i}") : imcNode); + Nodes.Add(globalContext.WithUIData ? imcNode.WithUIData(imcNode.Name ?? $"IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); + Nodes.Add(globalContext.WithUIData ? mdlNode.WithUIData(mdlNode.Name ?? $"Model #{i}", mdlNode.Icon) : mdlNode); } AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); @@ -92,15 +92,15 @@ private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanE var imc = (ResourceHandle*)subObject->IMCArray[i]; var imcNode = subObjectContext.CreateNodeFromImc(imc); if (imcNode != null) - subObjectNodes.Add(globalContext.WithNames - ? imcNode.WithName(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}") + subObjectNodes.Add(globalContext.WithUIData + ? imcNode.WithUIData(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - subObjectNodes.Add(globalContext.WithNames - ? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}") + subObjectNodes.Add(globalContext.WithUIData + ? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon) : mdlNode); } @@ -117,11 +117,11 @@ private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanE var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) - Nodes.Add(globalContext.WithNames ? decalNode.WithName(decalNode.Name ?? "Face Decal") : decalNode); + Nodes.Add(globalContext.WithUIData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : decalNode); var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) - Nodes.Add(globalContext.WithNames ? legacyDecalNode.WithName(legacyDecalNode.Name ?? "Legacy Body Decal") : legacyDecalNode); + Nodes.Add(globalContext.WithUIData ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) : legacyDecalNode); } private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") @@ -133,11 +133,11 @@ private unsafe void AddSkeleton(List nodes, ResolveContext context { var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) - nodes.Add(context.WithNames ? sklbNode.WithName($"{prefix}Skeleton #{i}") : sklbNode); + nodes.Add(context.WithUIData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); var skpNode = context.CreateParameterNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (skpNode != null) - nodes.Add(context.WithNames ? skpNode.WithName($"{prefix}Skeleton #{i} Parameters") : skpNode); + nodes.Add(context.WithUIData ? skpNode.WithUIData($"{prefix}Skeleton #{i} Parameters", skpNode.Icon) : skpNode); } } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index f416dc12..8d5318f2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -29,34 +29,35 @@ public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, Collecti _actors = actors; } - public ResourceTree[] FromObjectTable(bool withNames = true) + public ResourceTree[] FromObjectTable(bool withNames = true, bool redactExternalPaths = true) { var cache = new TreeBuildCache(_objects, _gameData); return cache.Characters - .Select(c => FromCharacter(c, cache, withNames)) + .Select(c => FromCharacter(c, cache, withNames, redactExternalPaths)) .OfType() .ToArray(); } public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, - bool withNames = true) + bool withUIData = true, bool redactExternalPaths = true) { var cache = new TreeBuildCache(_objects, _gameData); foreach (var character in characters) { - var tree = FromCharacter(character, cache, withNames); + var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); if (tree != null) yield return (character, tree); } } - public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withNames = true) - => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withNames); + public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withUIData = true, + bool redactExternalPaths = true) + => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withUIData, redactExternalPaths); private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, - bool withNames = true) + bool withUIData = true, bool redactExternalPaths = true) { if (!character.IsValid()) return null; @@ -73,7 +74,7 @@ public ResourceTree[] FromObjectTable(bool withNames = true) var (name, related) = GetCharacterName(character, cache); var tree = new ResourceTree(name, (nint)gameObjStruct, (nint)drawObjStruct, related, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, - ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withNames); + ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths); tree.LoadResources(globalContext); tree.FlatNodes.UnionWith(globalContext.Nodes.Values); return tree; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e90c148e..890abfed 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -554,7 +554,8 @@ private HashSet FindPathsStartingWith(ByteString prefix) public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents) + CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents, + ChangedItemDrawer changedItemDrawer) : base(WindowBaseLabel) { _performance = performance; @@ -581,7 +582,7 @@ public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialo (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); - _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 8e996eb7..528f19c1 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -14,7 +14,8 @@ namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer { private readonly Configuration _config; - private readonly ResourceTreeFactory _treeFactory; + private readonly ResourceTreeFactory _treeFactory; + private readonly ChangedItemDrawer _changedItemDrawer; private readonly int _actionCapacity; private readonly Action _onRefresh; private readonly Action _drawActions; @@ -22,15 +23,16 @@ public class ResourceTreeViewer private Task? _task; - public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, int actionCapacity, Action onRefresh, - Action drawActions) + public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, + int actionCapacity, Action onRefresh, Action drawActions) { - _config = config; - _treeFactory = treeFactory; - _actionCapacity = actionCapacity; - _onRefresh = onRefresh; - _drawActions = drawActions; - _unfolded = new HashSet(); + _config = config; + _treeFactory = treeFactory; + _changedItemDrawer = changedItemDrawer; + _actionCapacity = actionCapacity; + _onRefresh = onRefresh; + _drawActions = drawActions; + _unfolded = new HashSet(); } public void Draw() @@ -122,8 +124,24 @@ private void DrawNodes(IEnumerable resourceNodes, int level, nint ImGui.TableNextColumn(); var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) - { - ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); + { + if (resourceNode.Children.Count > 0) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var icon = (unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(); + var offset = (ImGui.GetFrameHeight() - ImGui.CalcTextSize(icon).X) / 2; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset); + ImGui.TextUnformatted(icon); + ImGui.SameLine(0f, offset + ImGui.GetStyle().ItemInnerSpacing.X); + } + else + { + ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); + ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); + } + _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); + ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.TableHeader(resourceNode.Name); if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) { if (unfolded) @@ -170,7 +188,9 @@ private void DrawNodes(IEnumerable resourceNodes, int level, nint ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in."); - } + } + + mutedColor.Dispose(); if (_actionCapacity > 0) { diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 7a81ec60..da4faa43 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -76,9 +76,11 @@ public bool FilterChangedItem(string name, object? data, LowerString filter) /// Draw the icon corresponding to the category of a changed item. public void DrawCategoryIcon(string name, object? data) + => DrawCategoryIcon(GetCategoryIcon(name, data)); + + public void DrawCategoryIcon(ChangedItemIcon iconType) { - var height = ImGui.GetFrameHeight(); - var iconType = GetCategoryIcon(name, data); + var height = ImGui.GetFrameHeight(); if (!_icons.TryGetValue(iconType, out var icon)) { ImGui.Dummy(new Vector2(height)); @@ -216,27 +218,13 @@ void DrawIcon(ChangedItemIcon type) } /// Obtain the icon category corresponding to a changed item. - private static ChangedItemIcon GetCategoryIcon(string name, object? obj) + internal static ChangedItemIcon GetCategoryIcon(string name, object? obj) { var iconType = ChangedItemIcon.Unknown; switch (obj) { case EquipItem it: - iconType = it.Type.ToSlot() switch - { - EquipSlot.MainHand => ChangedItemIcon.Mainhand, - EquipSlot.OffHand => ChangedItemIcon.Offhand, - EquipSlot.Head => ChangedItemIcon.Head, - EquipSlot.Body => ChangedItemIcon.Body, - EquipSlot.Hands => ChangedItemIcon.Hands, - EquipSlot.Legs => ChangedItemIcon.Legs, - EquipSlot.Feet => ChangedItemIcon.Feet, - EquipSlot.Ears => ChangedItemIcon.Ears, - EquipSlot.Neck => ChangedItemIcon.Neck, - EquipSlot.Wrists => ChangedItemIcon.Wrists, - EquipSlot.RFinger => ChangedItemIcon.Finger, - _ => ChangedItemIcon.Unknown, - }; + iconType = GetCategoryIcon(it.Type.ToSlot()); break; case ModelChara m: iconType = (CharacterBase.ModelType)m.Type switch @@ -259,6 +247,23 @@ private static ChangedItemIcon GetCategoryIcon(string name, object? obj) return iconType; } + internal static ChangedItemIcon GetCategoryIcon(EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => ChangedItemIcon.Mainhand, + EquipSlot.OffHand => ChangedItemIcon.Offhand, + EquipSlot.Head => ChangedItemIcon.Head, + EquipSlot.Body => ChangedItemIcon.Body, + EquipSlot.Hands => ChangedItemIcon.Hands, + EquipSlot.Legs => ChangedItemIcon.Legs, + EquipSlot.Feet => ChangedItemIcon.Feet, + EquipSlot.Ears => ChangedItemIcon.Ears, + EquipSlot.Neck => ChangedItemIcon.Neck, + EquipSlot.Wrists => ChangedItemIcon.Wrists, + EquipSlot.RFinger => ChangedItemIcon.Finger, + _ => ChangedItemIcon.Unknown, + }; + /// Return more detailed object information in text, if it exists. private static bool GetChangedItemObject(object? obj, out string text) { diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 0ebc7dbd..c8f86333 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -10,10 +10,10 @@ public class OnScreenTab : ITab private readonly Configuration _config; private ResourceTreeViewer _viewer; - public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory) + public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer) { _config = config; - _viewer = new ResourceTreeViewer(_config, treeFactory, 0, delegate { }, delegate { }); + _viewer = new ResourceTreeViewer(_config, treeFactory, changedItemDrawer, 0, delegate { }, delegate { }); } public ReadOnlySpan Label From a17a1e9576dca2c2e5fa4e699759bf73e1995d07 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 2 Sep 2023 17:16:33 +0200 Subject: [PATCH 4/6] Resource Tree: Make skp child of sklb --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 11 ++++++++++- Penumbra/Interop/ResourceTree/ResourceTree.cs | 4 ---- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 10 +++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 72a5be69..8e553091 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -283,12 +283,17 @@ static ushort GetTextureIndex(ushort texFlags) var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithUIData); if (node != null) + { + var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); + if (skpNode != null) + node.Children.Add(skpNode); Nodes.Add((nint)sklb->SkeletonResourceHandle, node); + } return node; } - public unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) { if (sklb->SkeletonParameterResourceHandle == null) return null; @@ -298,7 +303,11 @@ static ushort GetTextureIndex(ushort texFlags) var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithUIData); if (node != null) + { + if (WithUIData) + node = node.WithUIData("Skeleton Parameters", node.Icon); Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); + } return node; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index c6e90c6b..db3b287f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -134,10 +134,6 @@ private unsafe void AddSkeleton(List nodes, ResolveContext context var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) nodes.Add(context.WithUIData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); - - var skpNode = context.CreateParameterNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); - if (skpNode != null) - nodes.Add(context.WithUIData ? skpNode.WithUIData($"{prefix}Skeleton #{i} Parameters", skpNode.Icon) : skpNode); } } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 528f19c1..f0cf6030 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -8,7 +8,8 @@ using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; - +using System.Linq; + namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer @@ -125,7 +126,10 @@ private void DrawNodes(IEnumerable resourceNodes, int level, nint var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { - if (resourceNode.Children.Count > 0) + var unfoldable = debugMode + ? resourceNode.Children.Count > 0 + : resourceNode.Children.Any(child => !child.Internal); + if (unfoldable) { using var font = ImRaii.PushFont(UiBuilder.IconFont); var icon = (unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(); @@ -142,7 +146,7 @@ private void DrawNodes(IEnumerable resourceNodes, int level, nint _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.TableHeader(resourceNode.Name); - if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) + if (ImGui.IsItemClicked() && unfoldable) { if (unfolded) _unfolded.Remove(nodePathHash); From cca626449d134ab1b459d7b2000cb88e3ac0843d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 3 Sep 2023 05:50:51 +0200 Subject: [PATCH 5/6] Resource Tree: Fix shared model fold state --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index f0cf6030..90fcd820 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -85,7 +85,7 @@ public void Draw() (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - DrawNodes(tree.Nodes, 0, 0); + DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31)); } } } From 2a2fa3bf1d00ed84086040b7e05a72ead2ae5318 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 3 Sep 2023 13:13:35 +0200 Subject: [PATCH 6/6] Some auto-formatting and ROS iteration for lookups. --- OtterGui | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 118 +++++++++--------- Penumbra/Interop/ResourceTree/ResourceTree.cs | 14 +-- Penumbra/Interop/Structs/Material.cs | 4 + 4 files changed, 71 insertions(+), 67 deletions(-) diff --git a/OtterGui b/OtterGui index 728dd8c3..8c7a309d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 728dd8c33f8b43f7a2725ac7c8886fe7cb3f04a9 +Subproject commit 8c7a309d039fdf008c85cf51923b4eac51b32428 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 8e553091..24eea690 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -4,6 +4,7 @@ using System.Linq; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui; using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; @@ -15,19 +16,20 @@ namespace Penumbra.Interop.ResourceTree; -internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, - bool RedactExternalPaths) +internal record GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, + ModCollection Collection, int Skeleton, bool WithUiData, bool RedactExternalPaths) { public readonly Dictionary Nodes = new(128); public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithUIData, RedactExternalPaths, Nodes, slot, equipment); + => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithUiData, RedactExternalPaths, Nodes, slot, equipment); } -internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, - bool RedactExternalPaths, Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal record ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, + int Skeleton, bool WithUiData, bool RedactExternalPaths, Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); + private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) { if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) @@ -73,26 +75,28 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal); } - private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath, bool @internal) + private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, + Utf8GamePath gamePath, bool @internal) { - var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; if (fullPath.InternalName.IsEmpty) fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), + GetResourceHandleLength(resourceHandle), @internal); if (resourceHandle != null) Nodes.Add((nint)resourceHandle, node); return node; } - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, - bool withName) + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal, + bool withName) { - var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; - if (fullPath.InternalName.IsEmpty) - return null; - + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; + if (fullPath.InternalName.IsEmpty) + return null; + var gamePaths = Collection.ReverseResolvePath(fullPath).ToList(); fullPath = FilterFullPath(fullPath); @@ -100,14 +104,16 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec gamePaths = Filter(gamePaths); if (gamePaths.Count == 1) - return new ResourceNode(withName ? GuessUIDataFromPath(gamePaths[0]) : default, type, objectAddress, (nint)handle, gamePaths[0], fullPath, + return new ResourceNode(withName ? GuessUIDataFromPath(gamePaths[0]) : default, type, objectAddress, (nint)handle, gamePaths[0], + fullPath, GetResourceHandleLength(handle), @internal); Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); foreach (var gamePath in gamePaths) Penumbra.Log.Information($"Game path: {gamePath}"); - return new ResourceNode(default, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); + return new ResourceNode(default, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), + @internal); } public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) @@ -130,7 +136,7 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (node == null) return null; - if (WithUIData) + if (WithUiData) { var uiData = GuessModelUIData(node.GamePath); node = node.WithUIData(uiData.PrependName("IMC: ")); @@ -146,7 +152,7 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithUIData); + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithUiData); if (node != null) Nodes.Add((nint)tex, node); @@ -161,11 +167,11 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint) mdl, mdl->ResourceHandle, false, false); + var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, mdl->ResourceHandle, false, false); if (node == null) return null; - if (WithUIData) + if (WithUiData) node = node.WithUIData(GuessModelUIData(node.GamePath)); for (var i = 0; i < mdl->MaterialCount; i++) @@ -174,7 +180,7 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) // Don't keep the material's name if it's redundant with the model's name. - node.Children.Add(WithUIData + node.Children.Add(WithUiData ? mtrlNode.WithUIData((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) ?? $"Material #{i}", mtrlNode.Icon) : mtrlNode); @@ -191,33 +197,21 @@ static ushort GetTextureIndex(ushort texFlags) { if ((texFlags & 0x001F) != 0x001F) return (ushort)(texFlags & 0x001F); - else if ((texFlags & 0x03E0) != 0x03E0) + if ((texFlags & 0x03E0) != 0x03E0) return (ushort)((texFlags >> 5) & 0x001F); - else - return (ushort)((texFlags >> 10) & 0x001F); + + return (ushort)((texFlags >> 10) & 0x001F); } + static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle) - { - var textures = mtrl->Textures; - for (var i = 0; i < mtrl->TextureCount; ++i) - { - if (textures[i].ResourceHandle == handle) - return textures[i].Id; - } + => mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle, out var p) + ? p.Id + : null; - return null; - } static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) - { - var samplers = (ShaderPackageUtility.Sampler*)shpk->Samplers; - for (var i = 0; i < shpk->SamplerCount; ++i) - { - if (samplers[i].Id == id) - return samplers[i].Crc; - } - - return null; - } + => new ReadOnlySpan(shpk->Samplers, shpk->SamplerCount).FindFirst(s => s.Id == id, out var s) + ? s.Crc + : null; if (mtrl == null) return null; @@ -225,29 +219,30 @@ static ushort GetTextureIndex(ushort texFlags) var resource = mtrl->ResourceHandle; if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithUIData); + + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->Handle, false, WithUiData); if (node == null) return null; var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); if (shpkNode != null) - node.Children.Add(WithUIData ? shpkNode.WithUIData("Shader Package", 0) : shpkNode); - var shpkFile = WithUIData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var shpk = WithUIData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + node.Children.Add(WithUiData ? shpkNode.WithUIData("Shader Package", 0) : shpkNode); + var shpkFile = WithUiData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; + var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; for (var i = 0; i < resource->NumTex; i++) { - var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i)); + var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, + resource->TexIsDX11(i)); if (texNode == null) continue; - if (WithUIData) + if (WithUiData) { string? name = null; if (shpk != null) { - var index = GetTextureIndex(resource->TexSpace[i].Flags); + var index = GetTextureIndex(resource->TexSpace[i].Flags); uint? samplerId; if (index != 0x001F) samplerId = mtrl->Textures[index].Id; @@ -259,7 +254,8 @@ static ushort GetTextureIndex(ushort texFlags) if (samplerCrc.HasValue) name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; } - } + } + node.Children.Add(texNode.WithUIData(name ?? $"Texture #{i}", 0)); } else @@ -281,7 +277,8 @@ static ushort GetTextureIndex(ushort texFlags) if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithUIData); + var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, + WithUiData); if (node != null) { var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); @@ -301,10 +298,11 @@ static ushort GetTextureIndex(ushort texFlags) if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithUIData); + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, + WithUiData); if (node != null) { - if (WithUIData) + if (WithUiData) node = node.WithUIData("Skeleton Parameters", node.Icon); Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); } @@ -376,14 +374,16 @@ private ResourceNode.UIData GuessModelUIData(Utf8GamePath gamePath) _ => string.Empty, } + item.Name.ToString(); - return new(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); + return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); } var dataFromPath = GuessUIDataFromPath(gamePath); if (dataFromPath.Name != null) return dataFromPath; - return isEquipment ? new(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) : new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + return isEquipment + ? new ResourceNode.UIData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) + : new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } private ResourceNode.UIData GuessUIDataFromPath(Utf8GamePath gamePath) @@ -394,10 +394,10 @@ private ResourceNode.UIData GuessUIDataFromPath(Utf8GamePath gamePath) if (name.StartsWith("Customization:")) name = name[14..].Trim(); if (name != "Unknown") - return new(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); + return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); } - return new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + return new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index db3b287f..755103d7 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -56,12 +56,12 @@ internal unsafe void LoadResources(GlobalResolveContext globalContext) var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = context.CreateNodeFromImc(imc); if (imcNode != null) - Nodes.Add(globalContext.WithUIData ? imcNode.WithUIData(imcNode.Name ?? $"IMC #{i}", imcNode.Icon) : imcNode); + Nodes.Add(globalContext.WithUiData ? imcNode.WithUIData(imcNode.Name ?? $"IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - Nodes.Add(globalContext.WithUIData ? mdlNode.WithUIData(mdlNode.Name ?? $"Model #{i}", mdlNode.Icon) : mdlNode); + Nodes.Add(globalContext.WithUiData ? mdlNode.WithUIData(mdlNode.Name ?? $"Model #{i}", mdlNode.Icon) : mdlNode); } AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); @@ -92,14 +92,14 @@ private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanE var imc = (ResourceHandle*)subObject->IMCArray[i]; var imcNode = subObjectContext.CreateNodeFromImc(imc); if (imcNode != null) - subObjectNodes.Add(globalContext.WithUIData + subObjectNodes.Add(globalContext.WithUiData ? imcNode.WithUIData(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - subObjectNodes.Add(globalContext.WithUIData + subObjectNodes.Add(globalContext.WithUiData ? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon) : mdlNode); } @@ -117,11 +117,11 @@ private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanE var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) - Nodes.Add(globalContext.WithUIData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : decalNode); + Nodes.Add(globalContext.WithUiData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : decalNode); var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) - Nodes.Add(globalContext.WithUIData ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) : legacyDecalNode); + Nodes.Add(globalContext.WithUiData ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) : legacyDecalNode); } private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") @@ -133,7 +133,7 @@ private unsafe void AddSkeleton(List nodes, ResolveContext context { var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) - nodes.Add(context.WithUIData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); + nodes.Add(context.WithUiData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); } } } diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs index 7b66531c..3a204c75 100644 --- a/Penumbra/Interop/Structs/Material.cs +++ b/Penumbra/Interop/Structs/Material.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; @@ -41,4 +42,7 @@ public struct TextureEntry [FieldOffset( 0x10 )] public uint SamplerFlags; } + + public ReadOnlySpan TextureSpan + => new(Textures, TextureCount); } \ No newline at end of file