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.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -10,31 +12,42 @@ using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI;
namespace Penumbra.Interop.ResourceTree; 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) 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, internal record ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection,
CharacterArmor Equipment) 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 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) if (gamePath.IsEmpty)
return null; return null;
if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false))
return null; 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) if (dx11)
{ {
var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/'); var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/');
@ -59,13 +72,25 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
if (!Utf8GamePath.FromByteString(gamePath, out var path)) if (!Utf8GamePath.FromByteString(gamePath, out var path))
return null; 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) private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
=> new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal); 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) 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;
@ -79,14 +104,18 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
gamePaths = Filter(gamePaths); gamePaths = Filter(gamePaths);
if (gamePaths.Count == 1) 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}:"); Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:");
foreach (var gamePath in gamePaths) foreach (var gamePath in gamePaths)
Penumbra.Log.Information($"Game path: {gamePath}"); 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) public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr)
{ {
var raceSexIdStr = gr.ToRaceCode(); var raceSexIdStr = gr.ToRaceCode();
@ -95,38 +124,55 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
if (!Utf8GamePath.FromString(path, out var gamePath)) if (!Utf8GamePath.FromString(path, out var gamePath))
return null; return null;
return CreateNodeFromGamePath(ResourceType.Sklb, 0, gamePath, false); return CreateNodeFromGamePath(ResourceType.Sklb, 0, null, gamePath, false);
} }
public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) 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) if (node == null)
return null; return null;
if (WithNames) if (WithUiData)
{ {
var name = GuessModelName(node.GamePath); var uiData = GuessModelUIData(node.GamePath);
node = node.WithName(name != null ? $"IMC: {name}" : null); node = node.WithUIData(uiData.PrependName("IMC: "));
} }
Nodes.Add((nint)imc, node);
return node; return node;
} }
public unsafe ResourceNode? CreateNodeFromTex(ResourceHandle* tex) public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex)
=> CreateNodeFromResourceHandle(ResourceType.Tex, (nint) tex, tex, false, WithNames); {
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) public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl)
{ {
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
return null; return null;
var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint) mdl, mdl->ResourceHandle, false, false); if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached))
return cached;
var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, mdl->ResourceHandle, false, false);
if (node == null) if (node == null)
return null; return null;
if (WithNames) if (WithUiData)
node = node.WithName(GuessModelName(node.GamePath)); node = node.WithUIData(GuessModelUIData(node.GamePath));
for (var i = 0; i < mdl->MaterialCount; i++) for (var i = 0; i < mdl->MaterialCount; i++)
{ {
@ -134,43 +180,83 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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. // Don't keep the material's name if it's redundant with the model's name.
node.Children.Add(WithNames node.Children.Add(WithUiData
? mtrlNode.WithName((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) ? mtrlNode.WithUIData((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name)
?? $"Material #{i}") ?? $"Material #{i}", mtrlNode.Icon)
: mtrlNode); : mtrlNode);
} }
Nodes.Add((nint)mdl->ResourceHandle, node);
return node; return node;
} }
private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl) 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) if (mtrl == null)
return null; return null;
var resource = mtrl->ResourceHandle; 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) if (node == null)
return null; return null;
var mtrlFile = WithNames ? TreeBuildCache.ReadMaterial(node.FullPath) : null; var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false);
var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false);
if (shpkNode != null) if (shpkNode != null)
node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); node.Children.Add(WithUiData ? shpkNode.WithUIData("Shader Package", 0) : shpkNode);
var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; var shpkFile = WithUiData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null;
var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null; var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
for (var i = 0; i < resource->NumTex; i++) 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) if (texNode == null)
continue; continue;
if (WithNames) if (WithUiData)
{ {
var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null; string? name = null;
node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); 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 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; return node;
} }
@ -187,7 +316,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
return fullPath; return fullPath;
var relPath = Path.GetRelativePath(Config.ModDirectory, fullPath.FullName); 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.Exists ? fullPath : FullPath.Empty;
return FullPath.Empty; return FullPath.Empty;
@ -230,7 +359,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
} }
: false; : false;
private string? GuessModelName(Utf8GamePath gamePath) private 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.
@ -238,23 +367,26 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
if (isEquipment) if (isEquipment)
foreach (var item in Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot())) foreach (var item in Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot()))
{ {
return Slot switch var name = Slot switch
{ {
EquipSlot.RFinger => "R: ", EquipSlot.RFinger => "R: ",
EquipSlot.LFinger => "L: ", EquipSlot.LFinger => "L: ",
_ => string.Empty, _ => string.Empty,
} }
+ item.Name.ToString(); + item.Name.ToString();
return new ResourceNode.UIData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item));
} }
var nameFromPath = GuessNameFromPath(gamePath); var dataFromPath = GuessUIDataFromPath(gamePath);
if (nameFromPath != null) if (dataFromPath.Name != null)
return nameFromPath; 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())) foreach (var obj in Identifier.Identify(gamePath.ToString()))
{ {
@ -262,10 +394,10 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
if (name.StartsWith("Customization:")) if (name.StartsWith("Customization:"))
name = name[14..].Trim(); name = name[14..].Trim();
if (name != "Unknown") 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) private static string? SafeGet(ReadOnlySpan<string> array, Index index)
@ -294,4 +426,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
return name; 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 System.Collections.Generic;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon;
namespace Penumbra.Interop.ResourceTree; namespace Penumbra.Interop.ResourceTree;
public class ResourceNode public class ResourceNode
{ {
public readonly string? Name; public readonly string? Name;
public readonly ChangedItemIcon Icon;
public readonly ResourceType Type; public readonly ResourceType Type;
public readonly nint SourceAddress; public readonly nint ObjectAddress;
public readonly nint ResourceHandle;
public readonly Utf8GamePath GamePath; public readonly Utf8GamePath GamePath;
public readonly Utf8GamePath[] PossibleGamePaths; public readonly Utf8GamePath[] PossibleGamePaths;
public readonly FullPath FullPath; public readonly FullPath FullPath;
public readonly ulong Length;
public readonly bool Internal; public readonly bool Internal;
public readonly List<ResourceNode> Children; 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; Type = type;
SourceAddress = sourceAddress; ObjectAddress = objectAddress;
ResourceHandle = resourceHandle;
GamePath = gamePath; GamePath = gamePath;
PossibleGamePaths = new[] PossibleGamePaths = new[]
{ {
gamePath, gamePath,
}; };
FullPath = fullPath; FullPath = fullPath;
Length = length;
Internal = @internal; Internal = @internal;
Children = new List<ResourceNode>(); Children = new List<ResourceNode>();
} }
public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath[] possibleGamePaths, FullPath fullPath, public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath,
bool @internal) ulong length, bool @internal)
{ {
Name = name; Name = uiData.Name;
Icon = uiData.Icon;
Type = type; Type = type;
SourceAddress = sourceAddress; ObjectAddress = objectAddress;
ResourceHandle = resourceHandle;
GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty; GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty;
PossibleGamePaths = possibleGamePaths; PossibleGamePaths = possibleGamePaths;
FullPath = fullPath; FullPath = fullPath;
Length = length;
Internal = @internal; Internal = @internal;
Children = new List<ResourceNode>(); 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; Type = originalResourceNode.Type;
SourceAddress = originalResourceNode.SourceAddress; ObjectAddress = originalResourceNode.ObjectAddress;
ResourceHandle = originalResourceNode.ResourceHandle;
GamePath = originalResourceNode.GamePath; GamePath = originalResourceNode.GamePath;
PossibleGamePaths = originalResourceNode.PossibleGamePaths; PossibleGamePaths = originalResourceNode.PossibleGamePaths;
FullPath = originalResourceNode.FullPath; FullPath = originalResourceNode.FullPath;
Length = originalResourceNode.Length;
Internal = originalResourceNode.Internal; Internal = originalResourceNode.Internal;
Children = originalResourceNode.Children; Children = originalResourceNode.Children;
} }
public ResourceNode WithName(string? name) public ResourceNode WithUIData(string? name, ChangedItemIcon icon)
=> string.Equals(Name, name, StringComparison.Ordinal) ? this : new ResourceNode(name, this); => 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 System.Collections.Generic;
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 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;
@ -13,28 +14,32 @@ namespace Penumbra.Interop.ResourceTree;
public class ResourceTree public class ResourceTree
{ {
public readonly string Name; public readonly string Name;
public readonly nint SourceAddress; public readonly nint GameObjectAddress;
public readonly nint DrawObjectAddress;
public readonly bool PlayerRelated; public readonly bool PlayerRelated;
public readonly string CollectionName; public readonly string CollectionName;
public readonly List<ResourceNode> Nodes; public readonly List<ResourceNode> Nodes;
public readonly HashSet<ResourceNode> FlatNodes;
public int ModelId; public int ModelId;
public CustomizeData CustomizeData; public CustomizeData CustomizeData;
public GenderRace RaceCode; 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; Name = name;
SourceAddress = sourceAddress; GameObjectAddress = gameObjectAddress;
DrawObjectAddress = drawObjectAddress;
PlayerRelated = playerRelated; PlayerRelated = playerRelated;
CollectionName = collectionName; CollectionName = collectionName;
Nodes = new List<ResourceNode>(); Nodes = new List<ResourceNode>();
FlatNodes = new HashSet<ResourceNode>();
} }
internal unsafe void LoadResources(GlobalResolveContext globalContext) internal unsafe void LoadResources(GlobalResolveContext globalContext)
{ {
var character = (Character*)SourceAddress; var character = (Character*)GameObjectAddress;
var model = (CharacterBase*)character->GameObject.GetDrawObject(); var model = (CharacterBase*)DrawObjectAddress;
var equipment = new ReadOnlySpan<CharacterArmor>(&character->DrawData.Head, 10); var equipment = new ReadOnlySpan<CharacterArmor>(&character->DrawData.Head, 10);
// var customize = new ReadOnlySpan<byte>( character->CustomizeData, 26 ); // var customize = new ReadOnlySpan<byte>( character->CustomizeData, 26 );
ModelId = character->CharacterData.ModelCharaId; ModelId = character->CharacterData.ModelCharaId;
@ -51,14 +56,16 @@ 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.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 mdl = (RenderModel*)model->Models[i];
var mdlNode = context.CreateNodeFromRenderModel(mdl); var mdlNode = context.CreateNodeFromRenderModel(mdl);
if (mdlNode != null) 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) if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc)
AddHumanResources(globalContext, (HumanExt*)model); AddHumanResources(globalContext, (HumanExt*)model);
} }
@ -85,18 +92,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.WithNames subObjectNodes.Add(globalContext.WithUiData
? imcNode.WithName(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}") ? imcNode.WithUIData(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}", imcNode.Icon)
: imcNode); : 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.WithNames subObjectNodes.Add(globalContext.WithUiData
? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}") ? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon)
: mdlNode); : mdlNode);
} }
AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, ");
subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject;
++subObjectIndex; ++subObjectIndex;
} while (subObject != null && subObject != firstSubObject); } while (subObject != null && subObject != firstSubObject);
@ -106,16 +115,25 @@ public class ResourceTree
var context = globalContext.CreateContext(EquipSlot.Unknown, default); var context = globalContext.CreateContext(EquipSlot.Unknown, default);
var skeletonNode = context.CreateHumanSkeletonNode((GenderRace)human->Human.RaceSexId); var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal);
if (skeletonNode != null)
Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode);
var decalNode = context.CreateNodeFromTex(human->Decal);
if (decalNode != null) 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) 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; _actors = actors;
} }
public ResourceTree[] FromObjectTable(bool withNames = true) public ResourceTree[] FromObjectTable(bool withNames = true, bool redactExternalPaths = true)
{ {
var cache = new TreeBuildCache(_objects, _gameData); var cache = new TreeBuildCache(_objects, _gameData);
return cache.Characters return cache.Characters
.Select(c => FromCharacter(c, cache, withNames)) .Select(c => FromCharacter(c, cache, withNames, redactExternalPaths))
.OfType<ResourceTree>() .OfType<ResourceTree>()
.ToArray(); .ToArray();
} }
public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters(
IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> characters, IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> characters,
bool withNames = true) bool withUIData = true, bool redactExternalPaths = true)
{ {
var cache = new TreeBuildCache(_objects, _gameData); var cache = new TreeBuildCache(_objects, _gameData);
foreach (var character in characters) foreach (var character in characters)
{ {
var tree = FromCharacter(character, cache, withNames); var tree = FromCharacter(character, cache, withUIData, redactExternalPaths);
if (tree != null) if (tree != null)
yield return (character, tree); yield return (character, tree);
} }
} }
public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withNames = true) public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withUIData = true,
=> FromCharacter(character, new TreeBuildCache(_objects, _gameData), withNames); bool redactExternalPaths = true)
=> FromCharacter(character, new TreeBuildCache(_objects, _gameData), withUIData, redactExternalPaths);
private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, 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()) if (!character.IsValid())
return null; return null;
var gameObjStruct = (GameObject*)character.Address; var gameObjStruct = (GameObject*)character.Address;
if (gameObjStruct->GetDrawObject() == null) var drawObjStruct = gameObjStruct->GetDrawObject();
if (drawObjStruct == null)
return null; return null;
var collectionResolveData = _collectionResolver.IdentifyCollection(gameObjStruct, true); var collectionResolveData = _collectionResolver.IdentifyCollection(gameObjStruct, true);
@ -70,10 +72,11 @@ public class ResourceTreeFactory
return null; return null;
var (name, related) = GetCharacterName(character, cache); 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, 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.LoadResources(globalContext);
tree.FlatNodes.UnionWith(globalContext.Nodes.Values);
return tree; return tree;
} }

View file

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

View file

@ -1,3 +1,4 @@
using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
@ -41,4 +42,7 @@ public unsafe struct Material
[FieldOffset( 0x10 )] [FieldOffset( 0x10 )]
public uint SamplerFlags; 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, public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, 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) : base(WindowBaseLabel)
{ {
_performance = performance; _performance = performance;
@ -581,7 +582,7 @@ public partial class ModEditWindow : Window, IDisposable
(bytes, _, _) => new ShpkTab(_fileDialog, bytes)); (bytes, _, _) => new ShpkTab(_fileDialog, bytes));
_center = new CombinedTexture(_left, _right); _center = new CombinedTexture(_left, _right);
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); _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); _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow);
} }

View file

@ -8,6 +8,7 @@ using OtterGui.Raii;
using OtterGui; using OtterGui;
using Penumbra.Interop.ResourceTree; using Penumbra.Interop.ResourceTree;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
using System.Linq;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
@ -15,22 +16,24 @@ public class ResourceTreeViewer
{ {
private readonly Configuration _config; private readonly Configuration _config;
private readonly ResourceTreeFactory _treeFactory; private readonly ResourceTreeFactory _treeFactory;
private readonly ChangedItemDrawer _changedItemDrawer;
private readonly int _actionCapacity; private readonly int _actionCapacity;
private readonly Action _onRefresh; private readonly Action _onRefresh;
private readonly Action<ResourceNode, Vector2> _drawActions; private readonly Action<ResourceNode, Vector2> _drawActions;
private readonly HashSet<ResourceNode> _unfolded; private readonly HashSet<nint> _unfolded;
private Task<ResourceTree[]>? _task; private Task<ResourceTree[]>? _task;
public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, int actionCapacity, Action onRefresh, public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer,
Action<ResourceNode, Vector2> drawActions) int actionCapacity, Action onRefresh, Action<ResourceNode, Vector2> drawActions)
{ {
_config = config; _config = config;
_treeFactory = treeFactory; _treeFactory = treeFactory;
_changedItemDrawer = changedItemDrawer;
_actionCapacity = actionCapacity; _actionCapacity = actionCapacity;
_onRefresh = onRefresh; _onRefresh = onRefresh;
_drawActions = drawActions; _drawActions = drawActions;
_unfolded = new HashSet<ResourceNode>(); _unfolded = new HashSet<nint>();
} }
public void Draw() public void Draw()
@ -82,7 +85,7 @@ public class ResourceTreeViewer
(_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight());
ImGui.TableHeadersRow(); 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 debugMode = _config.DebugMode;
var frameHeight = ImGui.GetFrameHeight(); var frameHeight = ImGui.GetFrameHeight();
@ -111,24 +114,53 @@ public class ResourceTreeViewer
if (resourceNode.Internal && !debugMode) if (resourceNode.Internal && !debugMode)
continue; continue;
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
var textColorInternal = (textColor & 0x00FFFFFFu) | ((textColor & 0xFE000000u) >> 1); // Half opacity
using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal);
var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle);
using var id = ImRaii.PushId(index); using var id = ImRaii.PushId(index);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var unfolded = _unfolded.Contains(resourceNode); var unfolded = _unfolded.Contains(nodePathHash);
using (var indent = ImRaii.PushIndent(level)) using (var indent = ImRaii.PushIndent(level))
{ {
ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); var unfoldable = debugMode
if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) ? 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) if (unfolded)
_unfolded.Remove(resourceNode); _unfolded.Remove(nodePathHash);
else else
_unfolded.Add(resourceNode); _unfolded.Add(nodePathHash);
unfolded = !unfolded; unfolded = !unfolded;
} }
if (debugMode) if (debugMode)
{
using var _ = ImRaii.PushFont(UiBuilder.MonoFont);
ImGuiUtil.HoverTooltip( 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(); 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."); ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in.");
} }
mutedColor.Dispose();
if (_actionCapacity > 0) if (_actionCapacity > 0)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -171,7 +205,7 @@ public class ResourceTreeViewer
} }
if (unfolded) 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> /// <summary> Draw the icon corresponding to the category of a changed item. </summary>
public void DrawCategoryIcon(string name, object? data) public void DrawCategoryIcon(string name, object? data)
=> DrawCategoryIcon(GetCategoryIcon(name, data));
public void DrawCategoryIcon(ChangedItemIcon iconType)
{ {
var height = ImGui.GetFrameHeight(); var height = ImGui.GetFrameHeight();
var iconType = GetCategoryIcon(name, data);
if (!_icons.TryGetValue(iconType, out var icon)) if (!_icons.TryGetValue(iconType, out var icon))
{ {
ImGui.Dummy(new Vector2(height)); ImGui.Dummy(new Vector2(height));
@ -216,27 +218,13 @@ public class ChangedItemDrawer : IDisposable
} }
/// <summary> Obtain the icon category corresponding to a changed item. </summary> /// <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; var iconType = ChangedItemIcon.Unknown;
switch (obj) switch (obj)
{ {
case EquipItem it: case EquipItem it:
iconType = it.Type.ToSlot() switch iconType = GetCategoryIcon(it.Type.ToSlot());
{
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,
};
break; break;
case ModelChara m: case ModelChara m:
iconType = (CharacterBase.ModelType)m.Type switch iconType = (CharacterBase.ModelType)m.Type switch
@ -259,6 +247,23 @@ public class ChangedItemDrawer : IDisposable
return iconType; 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> /// <summary> Return more detailed object information in text, if it exists. </summary>
private static bool GetChangedItemObject(object? obj, out string text) private static bool GetChangedItemObject(object? obj, out string text)
{ {

View file

@ -10,10 +10,10 @@ public class OnScreenTab : ITab
private readonly Configuration _config; private readonly Configuration _config;
private ResourceTreeViewer _viewer; private ResourceTreeViewer _viewer;
public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory) public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer)
{ {
_config = config; _config = config;
_viewer = new ResourceTreeViewer(_config, treeFactory, 0, delegate { }, delegate { }); _viewer = new ResourceTreeViewer(_config, treeFactory, changedItemDrawer, 0, delegate { }, delegate { });
} }
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label