diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 0abf0cf5..de9ab5a7 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -17,6 +17,7 @@ using Penumbra.UI; using Penumbra.Collections.Manager; using Dalamud.Plugin.Services; using Penumbra.GameData.Enums; +using System.Diagnostics; namespace Penumbra.Api; @@ -1407,6 +1408,7 @@ public class IpcTester : IDisposable { private readonly DalamudPluginInterface _pi; private readonly IObjectTable _objects; + private readonly Stopwatch _stopwatch = new(); private string _gameObjectIndices = "0"; private ResourceType _type = ResourceType.Mtrl; @@ -1416,6 +1418,7 @@ public class IpcTester : IDisposable private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; + private TimeSpan _lastCallDuration; public ResourceTree(DalamudPluginInterface pi, IObjectTable objects) { @@ -1441,8 +1444,11 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourcePaths")) { var gameObjects = GetSelectedGameObjects(); - var resourcePaths = Ipc.GetGameObjectResourcePaths.Subscriber(_pi).Invoke(gameObjects); + var subscriber = Ipc.GetGameObjectResourcePaths.Subscriber(_pi); + _stopwatch.Restart(); + var resourcePaths = subscriber.Invoke(gameObjects); + _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourcePaths = gameObjects .Select(GameObjectToString) .Zip(resourcePaths) @@ -1454,7 +1460,12 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); if (ImGui.Button("Get##PlayerResourcePaths")) { - _lastPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Subscriber(_pi).Invoke() + var subscriber = Ipc.GetPlayerResourcePaths.Subscriber(_pi); + _stopwatch.Restart(); + var resourcePaths = subscriber.Invoke(); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourcePaths = resourcePaths .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) .ToArray(); @@ -1465,8 +1476,11 @@ public class IpcTester : IDisposable if (ImGui.Button("Get##GameObjectResourcesOfType")) { var gameObjects = GetSelectedGameObjects(); - var resourcesOfType = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi).Invoke(_type, _withUIData, gameObjects); + var subscriber = Ipc.GetGameObjectResourcesOfType.Subscriber(_pi); + _stopwatch.Restart(); + var resourcesOfType = subscriber.Invoke(_type, _withUIData, gameObjects); + _lastCallDuration = _stopwatch.Elapsed; _lastGameObjectResourcesOfType = gameObjects .Select(GameObjectToString) .Zip(resourcesOfType) @@ -1478,21 +1492,26 @@ public class IpcTester : IDisposable DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type"); if (ImGui.Button("Get##PlayerResourcesOfType")) { - _lastPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Subscriber(_pi).Invoke(_type, _withUIData) + var subscriber = Ipc.GetPlayerResourcesOfType.Subscriber(_pi); + _stopwatch.Restart(); + var resourcesOfType = subscriber.Invoke(_type, _withUIData); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourcesOfType = resourcesOfType .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) .ToArray(); ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType)); } - DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths); - DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths); + DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, _lastCallDuration); + DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration); - DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType); - DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType); + DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration); + DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration); } - private static void DrawPopup(string popupId, ref T? result, Action drawResult) where T : class + private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class { ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500)); using var popup = ImRaii.Popup(popupId); @@ -1510,6 +1529,8 @@ public class IpcTester : IDisposable drawResult(result); + ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms"); + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) { result = null; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 972e3c55..b6ce42d4 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -2,7 +2,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui; using Penumbra.Api.Enums; -using Penumbra.Collections; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -13,17 +12,17 @@ using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; -internal record GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, - ModCollection Collection, int Skeleton, bool WithUiData, bool RedactExternalPaths) +internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, + int Skeleton, bool WithUiData) { 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(Identifier, TreeBuildCache, Skeleton, WithUiData, Nodes, slot, equipment); } -internal record ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, - int Skeleton, bool WithUiData, bool RedactExternalPaths, Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, int Skeleton, bool WithUiData, + Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); @@ -76,52 +75,28 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie 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); + var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), @internal, this) + { + GamePath = gamePath, + FullPath = fullPath, + }; 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) { 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); - - if (gamePaths.Count > 1) - gamePaths = Filter(gamePaths); - - if (gamePaths.Count == 1) - 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); - } - - public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) - { - var raceSexIdStr = gr.ToRaceCode(); - var path = $"chara/human/c{raceSexIdStr}/skeleton/base/b0001/skl_c{raceSexIdStr}b0001.sklb"; - - if (!Utf8GamePath.FromString(path, out var gamePath)) - return null; - - return CreateNodeFromGamePath(ResourceType.Sklb, 0, null, gamePath, false); + return new ResourceNode(type, objectAddress, (nint)handle, GetResourceHandleLength(handle), @internal, this) + { + FullPath = fullPath, + }; } public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) @@ -129,16 +104,10 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie if (Nodes.TryGetValue((nint)imc, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false); + var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true); if (node == null) return null; - if (WithUiData) - { - var uiData = GuessModelUIData(node.GamePath); - node = node.WithUIData(uiData.PrependName("IMC: ")); - } - Nodes.Add((nint)imc, node); return node; @@ -149,7 +118,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie 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); if (node != null) Nodes.Add((nint)tex, node); @@ -164,23 +133,20 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie 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); if (node == null) return null; - if (WithUiData) - node = node.WithUIData(GuessModelUIData(node.GamePath)); - for (var i = 0; i < mdl->MaterialCount; i++) { var mtrl = (Material*)mdl->Materials[i]; 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 - ? mtrlNode.WithUIData((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) - ?? $"Material #{i}", mtrlNode.Icon) - : mtrlNode); + { + if (WithUiData) + mtrlNode.FallbackName = $"Material #{i}"; + node.Children.Add(mtrlNode); + } } Nodes.Add((nint)mdl->ResourceHandle, node); @@ -190,18 +156,20 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl) { - static ushort GetTextureIndex(ushort texFlags) + static ushort GetTextureIndex(Material* mtrl, ushort texFlags, HashSet alreadyVisitedSamplerIds) { - if ((texFlags & 0x001F) != 0x001F) + if ((texFlags & 0x001F) != 0x001F && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[texFlags & 0x001F].Id)) return (ushort)(texFlags & 0x001F); - if ((texFlags & 0x03E0) != 0x03E0) + if ((texFlags & 0x03E0) != 0x03E0 && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[(texFlags >> 5) & 0x001F].Id)) return (ushort)((texFlags >> 5) & 0x001F); + if ((texFlags & 0x7C00) != 0x7C00 && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[(texFlags >> 10) & 0x001F].Id)) + return (ushort)((texFlags >> 10) & 0x001F); - return (ushort)((texFlags >> 10) & 0x001F); + return 0x001F; } - static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle) - => mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle, out var p) + static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet alreadyVisitedSamplerIds) + => mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p) ? p.Id : null; @@ -217,16 +185,21 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie 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); 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); + { + if (WithUiData) + shpkNode.Name = "Shader Package"; + node.Children.Add(shpkNode); + } var shpkFile = WithUiData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->NumTex; i++) { var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, @@ -239,26 +212,26 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie string? name = null; if (shpk != null) { - var index = GetTextureIndex(resource->TexSpace[i].Flags); + var index = GetTextureIndex(mtrl, resource->TexSpace[i].Flags, alreadyProcessedSamplerIds); uint? samplerId; if (index != 0x001F) samplerId = mtrl->Textures[index].Id; else - samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle); + samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle, alreadyProcessedSamplerIds); if (samplerId.HasValue) { + alreadyProcessedSamplerIds.Add(samplerId.Value); 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 - { - node.Children.Add(texNode); + texNode = texNode.Clone(); + texNode.Name = name ?? $"Texture #{i}"; } + + node.Children.Add(texNode); } Nodes.Add((nint)resource, node); @@ -274,8 +247,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie 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); if (node != null) { var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); @@ -295,31 +267,18 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie 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); if (node != null) { if (WithUiData) - node = node.WithUIData("Skeleton Parameters", node.Icon); + node.FallbackName = "Skeleton Parameters"; Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); } return node; } - private FullPath FilterFullPath(FullPath fullPath) - { - if (!fullPath.IsRooted) - return fullPath; - - var relPath = Path.GetRelativePath(Config.ModDirectory, fullPath.FullName); - if (!RedactExternalPaths || relPath == "." || !relPath.StartsWith('.') && !Path.IsPathRooted(relPath)) - return fullPath.Exists ? fullPath : FullPath.Empty; - - return FullPath.Empty; - } - - private List Filter(List gamePaths) + internal List FilterGamePaths(List gamePaths) { var filtered = new List(gamePaths.Count); foreach (var path in gamePaths) @@ -356,7 +315,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie } : false; - private ResourceNode.UIData GuessModelUIData(Utf8GamePath gamePath) + internal ResourceNode.UiData GuessModelUIData(Utf8GamePath gamePath) { var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); // Weapons intentionally left out. @@ -371,7 +330,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie _ => string.Empty, } + item.Name.ToString(); - return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); + return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); } var dataFromPath = GuessUIDataFromPath(gamePath); @@ -379,11 +338,11 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie return dataFromPath; return isEquipment - ? new ResourceNode.UIData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) - : new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + ? new ResourceNode.UiData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) + : new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } - private ResourceNode.UIData GuessUIDataFromPath(Utf8GamePath gamePath) + internal ResourceNode.UiData GuessUIDataFromPath(Utf8GamePath gamePath) { foreach (var obj in Identifier.Identify(gamePath.ToString())) { @@ -391,10 +350,10 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie if (name.StartsWith("Customization:")) name = name[14..].Trim(); if (name != "Unknown") - return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); + return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); } - return new ResourceNode.UIData(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/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 2fffaedd..dfca5805 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -4,80 +4,89 @@ using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; namespace Penumbra.Interop.ResourceTree; -public class ResourceNode +public class ResourceNode : ICloneable { - public readonly string? Name; - public readonly ChangedItemIcon Icon; + public string? Name; + public string? FallbackName; + public ChangedItemIcon Icon; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; - public readonly Utf8GamePath GamePath; - public readonly Utf8GamePath[] PossibleGamePaths; - public readonly FullPath FullPath; + public Utf8GamePath[] PossibleGamePaths; + public FullPath FullPath; public readonly ulong Length; public readonly bool Internal; public readonly List Children; + internal ResolveContext? ResolveContext; - public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, - ulong length, bool @internal) + public Utf8GamePath GamePath { - Name = uiData.Name; - Icon = uiData.Icon; - Type = type; - ObjectAddress = objectAddress; - ResourceHandle = resourceHandle; - GamePath = gamePath; - PossibleGamePaths = new[] + get => PossibleGamePaths.Length == 1 ? PossibleGamePaths[0] : Utf8GamePath.Empty; + set { - gamePath, - }; - FullPath = fullPath; - Length = length; - Internal = @internal; - Children = new List(); + if (value.IsEmpty) + PossibleGamePaths = Array.Empty(); + else + PossibleGamePaths = new[] { value }; + } } - public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, - FullPath fullPath, - ulong length, bool @internal) + internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, bool @internal, ResolveContext? resolveContext) { - Name = uiData.Name; - Icon = uiData.Icon; Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; - GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty; - PossibleGamePaths = possibleGamePaths; - FullPath = fullPath; + PossibleGamePaths = Array.Empty(); Length = length; Internal = @internal; Children = new List(); + ResolveContext = resolveContext; } - private ResourceNode(UIData uiData, ResourceNode originalResourceNode) + private ResourceNode(ResourceNode other) { - Name = uiData.Name; - Icon = uiData.Icon; - Type = originalResourceNode.Type; - ObjectAddress = originalResourceNode.ObjectAddress; - ResourceHandle = originalResourceNode.ResourceHandle; - GamePath = originalResourceNode.GamePath; - PossibleGamePaths = originalResourceNode.PossibleGamePaths; - FullPath = originalResourceNode.FullPath; - Length = originalResourceNode.Length; - Internal = originalResourceNode.Internal; - Children = originalResourceNode.Children; + Name = other.Name; + FallbackName = other.FallbackName; + Icon = other.Icon; + Type = other.Type; + ObjectAddress = other.ObjectAddress; + ResourceHandle = other.ResourceHandle; + PossibleGamePaths = other.PossibleGamePaths; + FullPath = other.FullPath; + Length = other.Length; + Internal = other.Internal; + Children = other.Children; + ResolveContext = other.ResolveContext; } - public ResourceNode WithUIData(string? name, ChangedItemIcon icon) - => string.Equals(Name, name, StringComparison.Ordinal) && Icon == icon ? this : new ResourceNode(new UIData(name, icon), this); + public ResourceNode Clone() + => new(this); - public ResourceNode WithUIData(UIData uiData) - => string.Equals(Name, uiData.Name, StringComparison.Ordinal) && Icon == uiData.Icon ? this : new ResourceNode(uiData, this); + object ICloneable.Clone() + => Clone(); - public readonly record struct UIData(string? Name, ChangedItemIcon Icon) + public void ProcessPostfix(Action action, ResourceNode? parent) { - public readonly UIData PrependName(string prefix) - => Name == null ? this : new UIData(prefix + Name, Icon); + foreach (var child in Children) + child.ProcessPostfix(action, this); + action(this, parent); + } + + public void SetUiData(UiData uiData) + { + Name = uiData.Name; + Icon = uiData.Icon; + } + + public void PrependName(string prefix) + { + if (Name != null) + Name = prefix + Name; + } + + public readonly record struct UiData(string? Name, ChangedItemIcon Icon) + { + public readonly UiData PrependName(string prefix) + => Name == null ? this : new UiData(prefix + Name, Icon); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 161e0368..bc2cca26 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,10 +1,10 @@ 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; using Penumbra.Interop.Structs; +using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; namespace Penumbra.Interop.ResourceTree; @@ -40,6 +40,12 @@ public class ResourceTree FlatNodes = new HashSet(); } + public void ProcessPostfix(Action action) + { + foreach (var node in Nodes) + node.ProcessPostfix(action, null); + } + internal unsafe void LoadResources(GlobalResolveContext globalContext) { var character = (Character*)GameObjectAddress; @@ -60,12 +66,20 @@ public class ResourceTree 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); + { + if (globalContext.WithUiData) + imcNode.FallbackName = $"IMC #{i}"; + Nodes.Add(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); + { + if (globalContext.WithUiData) + mdlNode.FallbackName = $"Model #{i}"; + Nodes.Add(mdlNode); + } } AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); @@ -96,16 +110,20 @@ public class ResourceTree var imc = (ResourceHandle*)subObject->IMCArray[i]; var imcNode = subObjectContext.CreateNodeFromImc(imc); if (imcNode != null) - subObjectNodes.Add(globalContext.WithUiData - ? imcNode.WithUIData(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}", imcNode.Icon) - : imcNode); + { + if (globalContext.WithUiData) + imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}"; + subObjectNodes.Add(imcNode); + } var mdl = (RenderModel*)subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - subObjectNodes.Add(globalContext.WithUiData - ? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon) - : mdlNode); + { + if (globalContext.WithUiData) + mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}"; + subObjectNodes.Add(mdlNode); + } } AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); @@ -121,13 +139,27 @@ public class ResourceTree var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) - Nodes.Add(globalContext.WithUiData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : decalNode); + { + if (globalContext.WithUiData) + { + decalNode = decalNode.Clone(); + decalNode.FallbackName = "Face Decal"; + decalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + } + Nodes.Add(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); + { + if (globalContext.WithUiData) + { + legacyDecalNode = legacyDecalNode.Clone(); + legacyDecalNode.FallbackName = "Legacy Body Decal"; + legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + } + Nodes.Add(legacyDecalNode); + } } private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") @@ -139,7 +171,11 @@ public class ResourceTree { var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) - nodes.Add(context.WithUiData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); + { + if (context.WithUiData) + sklbNode.FallbackName = $"{prefix}Skeleton #{i}"; + nodes.Add(sklbNode); + } } } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 1d91948d..33a8de0f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,9 +1,12 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.Api.Enums; +using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.Interop.PathResolving; using Penumbra.Services; +using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; @@ -84,13 +87,126 @@ public class ResourceTreeFactory var (name, related) = GetCharacterName(character, cache); var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name); - var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, - ((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUIData) != 0, (flags & Flags.RedactExternalPaths) != 0); + var globalContext = new GlobalResolveContext(_identifier.AwaitedService, cache, + ((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUIData) != 0); tree.LoadResources(globalContext); tree.FlatNodes.UnionWith(globalContext.Nodes.Values); + tree.ProcessPostfix((node, _) => tree.FlatNodes.Add(node)); + + ResolveGamePaths(tree, collectionResolveData.ModCollection); + if (globalContext.WithUiData) + ResolveUiData(tree); + FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? _config.ModDirectory : null); + Cleanup(tree); + return tree; } + private static void ResolveGamePaths(ResourceTree tree, ModCollection collection) + { + var forwardSet = new HashSet(); + var reverseSet = new HashSet(); + foreach (var node in tree.FlatNodes) + { + if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty) + reverseSet.Add(node.FullPath.ToPath()); + else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1) + forwardSet.Add(node.GamePath); + } + + var forwardDictionary = forwardSet.ToDictionary(path => path, collection.ResolvePath); + var reverseArray = reverseSet.ToArray(); + var reverseResolvedArray = collection.ReverseResolvePaths(reverseArray); + var reverseDictionary = reverseArray.Zip(reverseResolvedArray).ToDictionary(pair => pair.First, pair => pair.Second); + + foreach (var node in tree.FlatNodes) + { + if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty) + { + if (reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet)) + { + var resolvedList = resolvedSet.ToList(); + if (resolvedList.Count > 1) + { + var filteredList = node.ResolveContext!.FilterGamePaths(resolvedList); + if (filteredList.Count > 0) + resolvedList = filteredList; + } + if (resolvedList.Count != 1) + { + Penumbra.Log.Information($"Found {resolvedList.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:"); + foreach (var gamePath in resolvedList) + Penumbra.Log.Information($"Game path: {gamePath}"); + } + node.PossibleGamePaths = resolvedList.ToArray(); + } + } + else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1) + { + if (forwardDictionary.TryGetValue(node.GamePath, out var resolved)) + node.FullPath = resolved ?? new FullPath(node.GamePath); + } + } + } + + private static void ResolveUiData(ResourceTree tree) + { + foreach (var node in tree.FlatNodes) + { + if (node.Name != null || node.PossibleGamePaths.Length == 0) + continue; + + var gamePath = node.PossibleGamePaths[0]; + node.SetUiData(node.Type switch + { + ResourceType.Imc => node.ResolveContext!.GuessModelUIData(gamePath).PrependName("IMC: "), + ResourceType.Mdl => node.ResolveContext!.GuessModelUIData(gamePath), + _ => node.ResolveContext!.GuessUIDataFromPath(gamePath), + }); + } + + tree.ProcessPostfix((node, parent) => + { + if (node.Name == parent?.Name) + node.Name = null; + }); + } + + private static void FilterFullPaths(ResourceTree tree, string? onlyWithinPath) + { + static bool ShallKeepPath(FullPath fullPath, string? onlyWithinPath) + { + if (!fullPath.IsRooted) + return true; + + if (onlyWithinPath != null) + { + var relPath = Path.GetRelativePath(onlyWithinPath, fullPath.FullName); + if (relPath != "." && (relPath.StartsWith('.') || Path.IsPathRooted(relPath))) + return false; + } + + return fullPath.Exists; + } + + foreach (var node in tree.FlatNodes) + { + if (!ShallKeepPath(node.FullPath, onlyWithinPath)) + node.FullPath = FullPath.Empty; + } + } + + private static void Cleanup(ResourceTree tree) + { + foreach (var node in tree.FlatNodes) + { + node.Name ??= node.FallbackName; + + node.FallbackName = null; + node.ResolveContext = null; + } + } + private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache) {