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 0cb854f3..24eea690 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.IO; 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; @@ -10,31 +12,42 @@ using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; +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 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, 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, 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 ResourceNode? CreateNodeFromShpk(nint sourceAddress, ByteString gamePath, bool @internal) + + 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)) 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 (Nodes.TryGetValue((nint)resourceHandle, out var cached)) + return cached; + if (dx11) { var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/'); @@ -59,19 +72,31 @@ 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); + + var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), + GetResourceHandleLength(resourceHandle), @internal); + if (resourceHandle != null) + Nodes.Add((nint)resourceHandle, node); - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, - bool withName) + return node; + } + + 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); @@ -79,14 +104,18 @@ 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 ? 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, sourceAddress, gamePaths.ToArray(), fullPath, @internal); + return new ResourceNode(default, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), + @internal); } + public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) { var raceSexIdStr = gr.ToRaceCode(); @@ -95,38 +124,55 @@ 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); + if (Nodes.TryGetValue((nint)imc, out var cached)) + return cached; + + var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false); 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); + return node; } - public unsafe ResourceNode? CreateNodeFromTex(ResourceHandle* tex) - => CreateNodeFromResourceHandle(ResourceType.Tex, (nint) tex, tex, false, WithNames); + public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) + { + if (Nodes.TryGetValue((nint)tex, out var cached)) + return cached; + + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithUiData); + 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; - var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint) mdl, mdl->ResourceHandle, false, false); + 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; - if (WithNames) - node = node.WithName(GuessModelName(node.GamePath)); + if (WithUiData) + node = node.WithUIData(GuessModelUIData(node.GamePath)); for (var i = 0; i < mdl->MaterialCount; i++) { @@ -134,43 +180,83 @@ private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddres 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); } + Nodes.Add((nint)mdl->ResourceHandle, node); + return node; } private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl) { + static ushort GetTextureIndex(ushort texFlags) + { + if ((texFlags & 0x001F) != 0x001F) + return (ushort)(texFlags & 0x001F); + if ((texFlags & 0x03E0) != 0x03E0) + return (ushort)((texFlags >> 5) & 0x001F); + + return (ushort)((texFlags >> 10) & 0x001F); + } + + static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle) + => mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle, out var p) + ? p.Id + : null; + + static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) + => new ReadOnlySpan(shpk->Samplers, shpk->SamplerCount).FindFirst(s => s.Id == id, out var s) + ? s.Crc + : null; + if (mtrl == null) return null; - var resource = mtrl->ResourceHandle; - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); + 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); 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; + 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(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) + if (WithUiData) { - var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null; - node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); + 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.WithUIData(name ?? $"Texture #{i}", 0)); } else { @@ -178,6 +264,49 @@ private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddres } } + Nodes.Add((nint)resource, node); + + return node; + } + + public unsafe ResourceNode? CreateNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + { + if (sklb->SkeletonResourceHandle == null) + return null; + + if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) + return cached; + + 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; + } + + private 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, + WithUiData); + if (node != null) + { + if (WithUiData) + node = node.WithUIData("Skeleton Parameters", node.Icon); + Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); + } + return node; } @@ -187,7 +316,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; @@ -230,7 +359,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. @@ -238,23 +367,26 @@ 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 ResourceNode.UIData(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 ResourceNode.UIData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) + : new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } - private string? GuessNameFromPath(Utf8GamePath gamePath) + private ResourceNode.UIData GuessUIDataFromPath(Utf8GamePath gamePath) { foreach (var obj in Identifier.Identify(gamePath.ToString())) { @@ -262,10 +394,10 @@ private List Filter(List gamePaths) if (name.StartsWith("Customization:")) name = name[14..].Trim(); if (name != "Unknown") - return name; + return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); } - return null; + return new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) @@ -294,4 +426,12 @@ internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle) return name; } + + private 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..17584787 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -2,60 +2,83 @@ 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 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(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, + ulong length, bool @internal) { - Name = name; - Type = type; - SourceAddress = sourceAddress; - GamePath = gamePath; + Name = uiData.Name; + Icon = uiData.Icon; + 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(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; - 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(); } - private ResourceNode(string? name, ResourceNode originalResourceNode) + private ResourceNode(UIData uiData, ResourceNode originalResourceNode) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; 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; } - 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 76d0c3f2..755103d7 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; @@ -12,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; @@ -51,13 +56,15 @@ 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); if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc) AddHumanResources(globalContext, (HumanExt*)model); @@ -85,17 +92,19 @@ 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); - } + } + + AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; ++subObjectIndex; @@ -106,16 +115,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); + Nodes.Add(globalContext.WithUiData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : 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); + 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 = "") + { + 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.WithUiData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); + } } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 98c1b305..8d5318f2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -29,40 +29,42 @@ 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; 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 +72,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); + ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths); tree.LoadResources(globalContext); + tree.FlatNodes.UnionWith(globalContext.Nodes.Values); return tree; } 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/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 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 4d8c77a7..90fcd820 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -8,29 +8,32 @@ using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; - +using System.Linq; + 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; - private readonly HashSet _unfolded; + private readonly HashSet _unfolded; 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() @@ -82,7 +85,7 @@ public void Draw() (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - DrawNodes(tree.Nodes, 0); + DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31)); } } } @@ -101,7 +104,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 +112,55 @@ 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) + { + 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(); + 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() && unfoldable) { 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}\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:X16}"); + } } ImGui.TableNextColumn(); @@ -160,7 +192,9 @@ private void DrawNodes(IEnumerable resourceNodes, int level) 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) { @@ -171,7 +205,7 @@ private void DrawNodes(IEnumerable resourceNodes, int level) } if (unfolded) - DrawNodes(resourceNode.Children, level + 1); + DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31)); } } } 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