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.GameData.Structs; 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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide 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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide 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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide 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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide 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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide : mtrlNode); } + Nodes.Add((nint)mdl->ResourceHandle, node); + return node; } @@ -188,7 +221,10 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide 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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide } } + Nodes.Add((nint)resource, node); + return node; } @@ -239,7 +277,29 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide 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 record class ResolveContext(Configuration Config, IObjectIdentifier Ide 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 @@ public class ResourceTree 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 class ResourceTreeFactory 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 class ResourceTreeFactory 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 class ResourceTreeViewer _actionCapacity = actionCapacity; _onRefresh = onRefresh; _drawActions = drawActions; - _unfolded = new HashSet(); + _unfolded = new HashSet(); } public void Draw() @@ -82,7 +82,7 @@ public class ResourceTreeViewer (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - DrawNodes(tree.Nodes, 0); + DrawNodes(tree.Nodes, 0, 0); } } } @@ -101,7 +101,7 @@ public class ResourceTreeViewer } }); - 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 @@ public class ResourceTreeViewer 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 @@ public class ResourceTreeViewer } if (unfolded) - DrawNodes(resourceNode.Children, level + 1); + DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31)); } } }