ResourceTree: Reverse-resolve in bulk

This commit is contained in:
Exter-N 2023-09-19 01:32:31 +02:00
parent 69012e5ecd
commit f02a37b939
5 changed files with 311 additions and 170 deletions

View file

@ -17,6 +17,7 @@ using Penumbra.UI;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using System.Diagnostics;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -1407,6 +1408,7 @@ public class IpcTester : IDisposable
{ {
private readonly DalamudPluginInterface _pi; private readonly DalamudPluginInterface _pi;
private readonly IObjectTable _objects; private readonly IObjectTable _objects;
private readonly Stopwatch _stopwatch = new();
private string _gameObjectIndices = "0"; private string _gameObjectIndices = "0";
private ResourceType _type = ResourceType.Mtrl; private ResourceType _type = ResourceType.Mtrl;
@ -1416,6 +1418,7 @@ public class IpcTester : IDisposable
private (string, IReadOnlyDictionary<string, string[]>?)[]? _lastPlayerResourcePaths; private (string, IReadOnlyDictionary<string, string[]>?)[]? _lastPlayerResourcePaths;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastGameObjectResourcesOfType; private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastGameObjectResourcesOfType;
private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastPlayerResourcesOfType; private (string, IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)[]? _lastPlayerResourcesOfType;
private TimeSpan _lastCallDuration;
public ResourceTree(DalamudPluginInterface pi, IObjectTable objects) public ResourceTree(DalamudPluginInterface pi, IObjectTable objects)
{ {
@ -1441,8 +1444,11 @@ public class IpcTester : IDisposable
if (ImGui.Button("Get##GameObjectResourcePaths")) if (ImGui.Button("Get##GameObjectResourcePaths"))
{ {
var gameObjects = GetSelectedGameObjects(); 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 _lastGameObjectResourcePaths = gameObjects
.Select(GameObjectToString) .Select(GameObjectToString)
.Zip(resourcePaths) .Zip(resourcePaths)
@ -1454,7 +1460,12 @@ public class IpcTester : IDisposable
DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths");
if (ImGui.Button("Get##PlayerResourcePaths")) 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<string, string[]>?)pair.Value)) .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary<string, string[]>?)pair.Value))
.ToArray(); .ToArray();
@ -1465,8 +1476,11 @@ public class IpcTester : IDisposable
if (ImGui.Button("Get##GameObjectResourcesOfType")) if (ImGui.Button("Get##GameObjectResourcesOfType"))
{ {
var gameObjects = GetSelectedGameObjects(); 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 _lastGameObjectResourcesOfType = gameObjects
.Select(GameObjectToString) .Select(GameObjectToString)
.Zip(resourcesOfType) .Zip(resourcesOfType)
@ -1478,21 +1492,26 @@ public class IpcTester : IDisposable
DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type"); DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type");
if (ImGui.Button("Get##PlayerResourcesOfType")) 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<nint, (string, string, ChangedItemIcon)>?)pair.Value)) .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary<nint, (string, string, ChangedItemIcon)>?)pair.Value))
.ToArray(); .ToArray();
ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType)); ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType));
} }
DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths); DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, _lastCallDuration);
DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths); DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration);
DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType); DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration);
DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType); DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration);
} }
private static void DrawPopup<T>(string popupId, ref T? result, Action<T> drawResult) where T : class private static void DrawPopup<T>(string popupId, ref T? result, Action<T> drawResult, TimeSpan duration) where T : class
{ {
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500)); ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500));
using var popup = ImRaii.Popup(popupId); using var popup = ImRaii.Popup(popupId);
@ -1510,6 +1529,8 @@ public class IpcTester : IDisposable
drawResult(result); drawResult(result);
ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms");
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
{ {
result = null; result = null;

View file

@ -2,7 +2,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui; using OtterGui;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -13,17 +12,17 @@ using Penumbra.UI;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
internal record GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache,
ModCollection Collection, int Skeleton, bool WithUiData, bool RedactExternalPaths) int Skeleton, bool WithUiData)
{ {
public readonly Dictionary<nint, ResourceNode> Nodes = new(128); public readonly Dictionary<nint, ResourceNode> Nodes = new(128);
public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) 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, internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, int Skeleton, bool WithUiData,
int Skeleton, bool WithUiData, bool RedactExternalPaths, Dictionary<nint, ResourceNode> Nodes, EquipSlot Slot, CharacterArmor Equipment) Dictionary<nint, ResourceNode> Nodes, EquipSlot Slot, CharacterArmor Equipment)
{ {
private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); 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) 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), var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), @internal, this)
GetResourceHandleLength(resourceHandle), @internal); {
GamePath = gamePath,
FullPath = fullPath,
};
if (resourceHandle != null) if (resourceHandle != null)
Nodes.Add((nint)resourceHandle, node); Nodes.Add((nint)resourceHandle, node);
return node; return node;
} }
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, 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; var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty;
if (fullPath.InternalName.IsEmpty) if (fullPath.InternalName.IsEmpty)
return null; return null;
var gamePaths = Collection.ReverseResolvePath(fullPath).ToList(); return new ResourceNode(type, objectAddress, (nint)handle, GetResourceHandleLength(handle), @internal, this)
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(); FullPath = fullPath,
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);
} }
public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) 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)) if (Nodes.TryGetValue((nint)imc, out var cached))
return cached; return cached;
var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false); var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true);
if (node == null) if (node == null)
return null; return null;
if (WithUiData)
{
var uiData = GuessModelUIData(node.GamePath);
node = node.WithUIData(uiData.PrependName("IMC: "));
}
Nodes.Add((nint)imc, node); Nodes.Add((nint)imc, node);
return node; return node;
@ -149,7 +118,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie
if (Nodes.TryGetValue((nint)tex, out var cached)) if (Nodes.TryGetValue((nint)tex, out var cached))
return 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) if (node != null)
Nodes.Add((nint)tex, node); 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)) if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached))
return 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) if (node == null)
return null; return null;
if (WithUiData)
node = node.WithUIData(GuessModelUIData(node.GamePath));
for (var i = 0; i < mdl->MaterialCount; i++) for (var i = 0; i < mdl->MaterialCount; i++)
{ {
var mtrl = (Material*)mdl->Materials[i]; var mtrl = (Material*)mdl->Materials[i];
var mtrlNode = CreateNodeFromMaterial(mtrl); var mtrlNode = CreateNodeFromMaterial(mtrl);
if (mtrlNode != null) if (mtrlNode != null)
// Don't keep the material's name if it's redundant with the model's name. {
node.Children.Add(WithUiData if (WithUiData)
? mtrlNode.WithUIData((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) mtrlNode.FallbackName = $"Material #{i}";
?? $"Material #{i}", mtrlNode.Icon) node.Children.Add(mtrlNode);
: mtrlNode); }
} }
Nodes.Add((nint)mdl->ResourceHandle, node); Nodes.Add((nint)mdl->ResourceHandle, node);
@ -190,18 +156,20 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie
private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl) private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl)
{ {
static ushort GetTextureIndex(ushort texFlags) static ushort GetTextureIndex(Material* mtrl, ushort texFlags, HashSet<uint> alreadyVisitedSamplerIds)
{ {
if ((texFlags & 0x001F) != 0x001F) if ((texFlags & 0x001F) != 0x001F && !alreadyVisitedSamplerIds.Contains(mtrl->Textures[texFlags & 0x001F].Id))
return (ushort)(texFlags & 0x001F); 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); 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) static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet<uint> alreadyVisitedSamplerIds)
=> mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle, out var p) => mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p)
? p.Id ? p.Id
: null; : null;
@ -217,16 +185,21 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie
if (Nodes.TryGetValue((nint)resource, out var cached)) if (Nodes.TryGetValue((nint)resource, out var cached))
return 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) if (node == null)
return null; return null;
var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false);
if (shpkNode != null) 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 shpkFile = WithUiData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null;
var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
var alreadyProcessedSamplerIds = new HashSet<uint>();
for (var i = 0; i < resource->NumTex; i++) for (var i = 0; i < resource->NumTex; i++)
{ {
var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false,
@ -239,27 +212,27 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie
string? name = null; string? name = null;
if (shpk != null) if (shpk != null)
{ {
var index = GetTextureIndex(resource->TexSpace[i].Flags); var index = GetTextureIndex(mtrl, resource->TexSpace[i].Flags, alreadyProcessedSamplerIds);
uint? samplerId; uint? samplerId;
if (index != 0x001F) if (index != 0x001F)
samplerId = mtrl->Textures[index].Id; samplerId = mtrl->Textures[index].Id;
else else
samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle); samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle, alreadyProcessedSamplerIds);
if (samplerId.HasValue) if (samplerId.HasValue)
{ {
alreadyProcessedSamplerIds.Add(samplerId.Value);
var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value);
if (samplerCrc.HasValue) if (samplerCrc.HasValue)
name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}";
} }
} }
node.Children.Add(texNode.WithUIData(name ?? $"Texture #{i}", 0)); texNode = texNode.Clone();
texNode.Name = name ?? $"Texture #{i}";
} }
else
{
node.Children.Add(texNode); node.Children.Add(texNode);
} }
}
Nodes.Add((nint)resource, node); 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)) if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached))
return cached; return cached;
var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false);
WithUiData);
if (node != null) if (node != null)
{ {
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); var skpNode = CreateParameterNodeFromPartialSkeleton(sklb);
@ -295,31 +267,18 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie
if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached))
return cached; return cached;
var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true);
WithUiData);
if (node != null) if (node != null)
{ {
if (WithUiData) if (WithUiData)
node = node.WithUIData("Skeleton Parameters", node.Icon); node.FallbackName = "Skeleton Parameters";
Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node);
} }
return node; return node;
} }
private FullPath FilterFullPath(FullPath fullPath) internal List<Utf8GamePath> FilterGamePaths(List<Utf8GamePath> gamePaths)
{
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<Utf8GamePath> Filter(List<Utf8GamePath> gamePaths)
{ {
var filtered = new List<Utf8GamePath>(gamePaths.Count); var filtered = new List<Utf8GamePath>(gamePaths.Count);
foreach (var path in gamePaths) foreach (var path in gamePaths)
@ -356,7 +315,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie
} }
: false; : false;
private ResourceNode.UIData GuessModelUIData(Utf8GamePath gamePath) internal ResourceNode.UiData GuessModelUIData(Utf8GamePath gamePath)
{ {
var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries);
// Weapons intentionally left out. // Weapons intentionally left out.
@ -371,7 +330,7 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie
_ => string.Empty, _ => string.Empty,
} }
+ item.Name.ToString(); + 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); var dataFromPath = GuessUIDataFromPath(gamePath);
@ -379,11 +338,11 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie
return dataFromPath; return dataFromPath;
return isEquipment return isEquipment
? new ResourceNode.UIData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) ? new ResourceNode.UiData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot()))
: new ResourceNode.UIData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); : 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())) foreach (var obj in Identifier.Identify(gamePath.ToString()))
{ {
@ -391,10 +350,10 @@ internal record ResolveContext(Configuration Config, IObjectIdentifier Identifie
if (name.StartsWith("Customization:")) if (name.StartsWith("Customization:"))
name = name[14..].Trim(); name = name[14..].Trim();
if (name != "Unknown") 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<string> array, Index index) private static string? SafeGet(ReadOnlySpan<string> array, Index index)

View file

@ -4,80 +4,89 @@ using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
public class ResourceNode public class ResourceNode : ICloneable
{ {
public readonly string? Name; public string? Name;
public readonly ChangedItemIcon Icon; public string? FallbackName;
public ChangedItemIcon Icon;
public readonly ResourceType Type; public readonly ResourceType Type;
public readonly nint ObjectAddress; public readonly nint ObjectAddress;
public readonly nint ResourceHandle; public readonly nint ResourceHandle;
public readonly Utf8GamePath GamePath; public Utf8GamePath[] PossibleGamePaths;
public readonly Utf8GamePath[] PossibleGamePaths; public FullPath FullPath;
public readonly FullPath FullPath;
public readonly ulong Length; public readonly ulong Length;
public readonly bool Internal; public readonly bool Internal;
public readonly List<ResourceNode> Children; public readonly List<ResourceNode> Children;
internal ResolveContext? ResolveContext;
public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, public Utf8GamePath GamePath
ulong length, bool @internal) {
get => PossibleGamePaths.Length == 1 ? PossibleGamePaths[0] : Utf8GamePath.Empty;
set
{
if (value.IsEmpty)
PossibleGamePaths = Array.Empty<Utf8GamePath>();
else
PossibleGamePaths = new[] { value };
}
}
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, bool @internal, ResolveContext? resolveContext)
{ {
Name = uiData.Name;
Icon = uiData.Icon;
Type = type; Type = type;
ObjectAddress = objectAddress; ObjectAddress = objectAddress;
ResourceHandle = resourceHandle; ResourceHandle = resourceHandle;
GamePath = gamePath; PossibleGamePaths = Array.Empty<Utf8GamePath>();
PossibleGamePaths = new[]
{
gamePath,
};
FullPath = fullPath;
Length = length; Length = length;
Internal = @internal; Internal = @internal;
Children = new List<ResourceNode>(); Children = new List<ResourceNode>();
ResolveContext = resolveContext;
} }
public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, private ResourceNode(ResourceNode other)
FullPath fullPath, {
ulong length, bool @internal) 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 Clone()
=> new(this);
object ICloneable.Clone()
=> Clone();
public void ProcessPostfix(Action<ResourceNode, ResourceNode?> action, ResourceNode? parent)
{
foreach (var child in Children)
child.ProcessPostfix(action, this);
action(this, parent);
}
public void SetUiData(UiData uiData)
{ {
Name = uiData.Name; Name = uiData.Name;
Icon = uiData.Icon; Icon = uiData.Icon;
Type = type;
ObjectAddress = objectAddress;
ResourceHandle = resourceHandle;
GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty;
PossibleGamePaths = possibleGamePaths;
FullPath = fullPath;
Length = length;
Internal = @internal;
Children = new List<ResourceNode>();
} }
private ResourceNode(UIData uiData, ResourceNode originalResourceNode) public void PrependName(string prefix)
{ {
Name = uiData.Name; if (Name != null)
Icon = uiData.Icon; Name = prefix + Name;
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;
} }
public ResourceNode WithUIData(string? name, ChangedItemIcon icon) public readonly record struct UiData(string? Name, ChangedItemIcon Icon)
=> string.Equals(Name, name, StringComparison.Ordinal) && Icon == icon ? this : new ResourceNode(new UIData(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) public readonly UiData PrependName(string prefix)
=> Name == null ? this : new UIData(prefix + Name, Icon); => Name == null ? this : new UiData(prefix + Name, Icon);
} }
} }

View file

@ -1,10 +1,10 @@
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.UI;
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
@ -40,6 +40,12 @@ public class ResourceTree
FlatNodes = new HashSet<ResourceNode>(); FlatNodes = new HashSet<ResourceNode>();
} }
public void ProcessPostfix(Action<ResourceNode, ResourceNode?> action)
{
foreach (var node in Nodes)
node.ProcessPostfix(action, null);
}
internal unsafe void LoadResources(GlobalResolveContext globalContext) internal unsafe void LoadResources(GlobalResolveContext globalContext)
{ {
var character = (Character*)GameObjectAddress; var character = (Character*)GameObjectAddress;
@ -60,12 +66,20 @@ public class ResourceTree
var imc = (ResourceHandle*)model->IMCArray[i]; var imc = (ResourceHandle*)model->IMCArray[i];
var imcNode = context.CreateNodeFromImc(imc); var imcNode = context.CreateNodeFromImc(imc);
if (imcNode != null) 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 mdl = (RenderModel*)model->Models[i];
var mdlNode = context.CreateNodeFromRenderModel(mdl); var mdlNode = context.CreateNodeFromRenderModel(mdl);
if (mdlNode != null) 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); AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton);
@ -96,16 +110,20 @@ public class ResourceTree
var imc = (ResourceHandle*)subObject->IMCArray[i]; var imc = (ResourceHandle*)subObject->IMCArray[i];
var imcNode = subObjectContext.CreateNodeFromImc(imc); var imcNode = subObjectContext.CreateNodeFromImc(imc);
if (imcNode != null) if (imcNode != null)
subObjectNodes.Add(globalContext.WithUiData {
? imcNode.WithUIData(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}", imcNode.Icon) if (globalContext.WithUiData)
: imcNode); imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}";
subObjectNodes.Add(imcNode);
}
var mdl = (RenderModel*)subObject->Models[i]; var mdl = (RenderModel*)subObject->Models[i];
var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl);
if (mdlNode != null) if (mdlNode != null)
subObjectNodes.Add(globalContext.WithUiData {
? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon) if (globalContext.WithUiData)
: mdlNode); mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}";
subObjectNodes.Add(mdlNode);
}
} }
AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, ");
@ -121,13 +139,27 @@ public class ResourceTree
var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal);
if (decalNode != null) 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); var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal);
if (legacyDecalNode != null) if (legacyDecalNode != null)
Nodes.Add(globalContext.WithUiData {
? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) if (globalContext.WithUiData)
: legacyDecalNode); {
legacyDecalNode = legacyDecalNode.Clone();
legacyDecalNode.FallbackName = "Legacy Body Decal";
legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization;
}
Nodes.Add(legacyDecalNode);
}
} }
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, Skeleton* skeleton, string prefix = "")
@ -139,7 +171,11 @@ public class ResourceTree
{ {
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]);
if (sklbNode != null) 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);
}
} }
} }
} }

View file

@ -1,9 +1,12 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.Interop.PathResolving; using Penumbra.Interop.PathResolving;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
@ -84,13 +87,126 @@ public class ResourceTreeFactory
var (name, related) = GetCharacterName(character, cache); var (name, related) = GetCharacterName(character, cache);
var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; 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 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, var globalContext = new GlobalResolveContext(_identifier.AwaitedService, cache,
((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUIData) != 0, (flags & Flags.RedactExternalPaths) != 0); ((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUIData) != 0);
tree.LoadResources(globalContext); tree.LoadResources(globalContext);
tree.FlatNodes.UnionWith(globalContext.Nodes.Values); 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; return tree;
} }
private static void ResolveGamePaths(ResourceTree tree, ModCollection collection)
{
var forwardSet = new HashSet<Utf8GamePath>();
var reverseSet = new HashSet<string>();
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, private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character,
TreeBuildCache cache) TreeBuildCache cache)
{ {