mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Merge branch 'restree-less-io'
This commit is contained in:
commit
176956a1f8
11 changed files with 383 additions and 160 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit 728dd8c33f8b43f7a2725ac7c8886fe7cb3f04a9
|
Subproject commit 8c7a309d039fdf008c85cf51923b4eac51b32428
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue