Merge branch 'restree-less-io'

This commit is contained in:
Ottermandias 2023-09-03 13:13:54 +02:00
commit 176956a1f8
11 changed files with 383 additions and 160 deletions

@ -1 +1 @@
Subproject commit 728dd8c33f8b43f7a2725ac7c8886fe7cb3f04a9
Subproject commit 8c7a309d039fdf008c85cf51923b4eac51b32428

View file

@ -2,7 +2,9 @@ using System;
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.GameData.Structs;
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<nint, ResourceNode> 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<nint, ResourceNode> 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,13 +72,25 @@ 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);
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal,
var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath),
GetResourceHandleLength(resourceHandle), @internal);
if (resourceHandle != null)
Nodes.Add((nint)resourceHandle, node);
return node;
}
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal,
bool withName)
{
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty;
@ -79,14 +104,18 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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;
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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<ShaderPackageUtility.Sampler>(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);
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
}
}
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
}
: 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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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<string> array, Index index)
@ -294,4 +426,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
return name;
}
private static unsafe ulong GetResourceHandleLength(ResourceHandle* handle)
{
if (handle == null)
return 0;
return ResourceHandle.GetLength(handle);
}
}

View file

@ -2,60 +2,83 @@ using System;
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<ResourceNode> 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;
Name = uiData.Name;
Icon = uiData.Icon;
Type = type;
SourceAddress = sourceAddress;
ObjectAddress = objectAddress;
ResourceHandle = resourceHandle;
GamePath = gamePath;
PossibleGamePaths = new[]
{
gamePath,
};
FullPath = fullPath;
Length = length;
Internal = @internal;
Children = new List<ResourceNode>();
}
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<ResourceNode>();
}
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);
}
}

View file

@ -2,6 +2,7 @@ using System;
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;
@ -13,28 +14,32 @@ namespace Penumbra.Interop.ResourceTree;
public class ResourceTree
{
public readonly string Name;
public readonly nint SourceAddress;
public readonly nint GameObjectAddress;
public readonly nint DrawObjectAddress;
public readonly bool PlayerRelated;
public readonly string CollectionName;
public readonly List<ResourceNode> Nodes;
public readonly HashSet<ResourceNode> 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;
GameObjectAddress = gameObjectAddress;
DrawObjectAddress = drawObjectAddress;
PlayerRelated = playerRelated;
CollectionName = collectionName;
Nodes = new List<ResourceNode>();
FlatNodes = new HashSet<ResourceNode>();
}
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<CharacterArmor>(&character->DrawData.Head, 10);
// var customize = new ReadOnlySpan<byte>( character->CustomizeData, 26 );
ModelId = character->CharacterData.ModelCharaId;
@ -51,14 +56,16 @@ public class ResourceTree
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,18 +92,20 @@ public class ResourceTree
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;
} while (subObject != null && subObject != firstSubObject);
@ -106,16 +115,25 @@ public class ResourceTree
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<ResourceNode> 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);
}
}
}

View file

@ -29,40 +29,42 @@ public class ResourceTreeFactory
_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<ResourceTree>()
.ToArray();
}
public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters(
IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> 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 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);
((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths);
tree.LoadResources(globalContext);
tree.FlatNodes.UnionWith(globalContext.Nodes.Values);
return tree;
}

View file

@ -12,7 +12,6 @@ namespace Penumbra.Interop.ResourceTree;
internal class TreeBuildCache
{
private readonly IDataManager _dataManager;
private readonly Dictionary<FullPath, MtrlFile?> _materials = new();
private readonly Dictionary<FullPath, ShpkFile?> _shaderPackages = new();
public readonly List<Character> Characters;
public readonly Dictionary<uint, Character> CharactersById;
@ -27,10 +26,6 @@ internal class TreeBuildCache
.ToDictionary(c => c.Key, c => c.First());
}
/// <summary> Try to read a material file from the given path and cache it on success. </summary>
public MtrlFile? ReadMaterial(FullPath path)
=> ReadFile(_dataManager, path, _materials, bytes => new MtrlFile(bytes));
/// <summary> Try to read a shpk file from the given path and cache it on success. </summary>
public ShpkFile? ReadShaderPackage(FullPath path)
=> ReadFile(_dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes));

View file

@ -1,3 +1,4 @@
using System;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
@ -41,4 +42,7 @@ public unsafe struct Material
[FieldOffset( 0x10 )]
public uint SamplerFlags;
}
public ReadOnlySpan<TextureEntry> TextureSpan
=> new(Textures, TextureCount);
}

View file

@ -554,7 +554,8 @@ public partial class ModEditWindow : Window, IDisposable
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 partial class ModEditWindow : Window, IDisposable
(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);
}

View file

@ -8,6 +8,7 @@ using OtterGui.Raii;
using OtterGui;
using Penumbra.Interop.ResourceTree;
using Penumbra.UI.Classes;
using System.Linq;
namespace Penumbra.UI.AdvancedWindow;
@ -15,22 +16,24 @@ public class ResourceTreeViewer
{
private readonly Configuration _config;
private readonly ResourceTreeFactory _treeFactory;
private readonly ChangedItemDrawer _changedItemDrawer;
private readonly int _actionCapacity;
private readonly Action _onRefresh;
private readonly Action<ResourceNode, Vector2> _drawActions;
private readonly HashSet<ResourceNode> _unfolded;
private readonly HashSet<nint> _unfolded;
private Task<ResourceTree[]>? _task;
public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, int actionCapacity, Action onRefresh,
Action<ResourceNode, Vector2> drawActions)
public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer,
int actionCapacity, Action onRefresh, Action<ResourceNode, Vector2> drawActions)
{
_config = config;
_treeFactory = treeFactory;
_changedItemDrawer = changedItemDrawer;
_actionCapacity = actionCapacity;
_onRefresh = onRefresh;
_drawActions = drawActions;
_unfolded = new HashSet<ResourceNode>();
_unfolded = new HashSet<nint>();
}
public void Draw()
@ -82,7 +85,7 @@ public class ResourceTreeViewer
(_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 @@ public class ResourceTreeViewer
}
});
private void DrawNodes(IEnumerable<ResourceNode> resourceNodes, int level)
private void DrawNodes(IEnumerable<ResourceNode> resourceNodes, int level, nint pathHash)
{
var debugMode = _config.DebugMode;
var frameHeight = ImGui.GetFrameHeight();
@ -111,24 +114,53 @@ public class ResourceTreeViewer
if (resourceNode.Internal && !debugMode)
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)
{
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();
@ -162,6 +194,8 @@ public class ResourceTreeViewer
ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in.");
}
mutedColor.Dispose();
if (_actionCapacity > 0)
{
ImGui.TableNextColumn();
@ -171,7 +205,7 @@ public class ResourceTreeViewer
}
if (unfolded)
DrawNodes(resourceNode.Children, level + 1);
DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31));
}
}
}

View file

@ -76,9 +76,11 @@ public class ChangedItemDrawer : IDisposable
/// <summary> Draw the icon corresponding to the category of a changed item. </summary>
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);
if (!_icons.TryGetValue(iconType, out var icon))
{
ImGui.Dummy(new Vector2(height));
@ -216,27 +218,13 @@ public class ChangedItemDrawer : IDisposable
}
/// <summary> Obtain the icon category corresponding to a changed item. </summary>
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 @@ public class ChangedItemDrawer : IDisposable
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,
};
/// <summary> Return more detailed object information in text, if it exists. </summary>
private static bool GetChangedItemObject(object? obj, out string text)
{

View file

@ -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<byte> Label