diff --git a/Penumbra.GameData b/Penumbra.GameData
index 15e7c8eb..73010350 160000
--- a/Penumbra.GameData
+++ b/Penumbra.GameData
@@ -1 +1 @@
-Subproject commit 15e7c8eb41867e6bbd3fe6a8885404df087bc7e7
+Subproject commit 73010350338ecd7b98ad85d127bed08d7d8718d4
diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs
index 4487eb7f..6be1b959 100644
--- a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs
+++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs
@@ -40,7 +40,7 @@ public static unsafe class SkinMtrlPathEarlyProcessing
if (character->TempSlotData is not null)
{
- // TODO ClientStructs-ify
+ // TODO ClientStructs-ify (aers/FFXIVClientStructs#1564)
var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8);
if (handle != null)
return handle;
diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs
index b6d04769..c204f141 100644
--- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs
+++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs
@@ -338,6 +338,34 @@ internal partial record ResolveContext
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
+ private Utf8GamePath ResolveKineDriverModulePath(uint partialSkeletonIndex)
+ {
+ // Correctness and Safety:
+ // Resolving a KineDriver module path through the game's code can use EST metadata for human skeletons.
+ // Additionally, it can dereference null pointers for human equipment skeletons.
+ return ModelType switch
+ {
+ ModelType.Human => ResolveHumanKineDriverModulePath(partialSkeletonIndex),
+ _ => ResolveKineDriverModulePathNative(partialSkeletonIndex),
+ };
+ }
+
+ private Utf8GamePath ResolveHumanKineDriverModulePath(uint partialSkeletonIndex)
+ {
+ var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
+ if (set.Id is 0)
+ return Utf8GamePath.Empty;
+
+ var path = GamePaths.Kdb.Customization(raceCode, slot, set);
+ return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
+ }
+
+ private unsafe Utf8GamePath ResolveKineDriverModulePathNative(uint partialSkeletonIndex)
+ {
+ var path = CharacterBase->ResolveKdbPathAsByteString(partialSkeletonIndex);
+ return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
+ }
+
private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc)
{
var animation = ResolveImcData(imc).MaterialAnimationId;
diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs
index b2364e33..bbe9b8ce 100644
--- a/Penumbra/Interop/ResourceTree/ResolveContext.cs
+++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs
@@ -371,7 +371,8 @@ internal unsafe partial record ResolveContext(
return node;
}
- public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex)
+ public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, ResourceHandle* kdbHandle,
+ uint partialSkeletonIndex)
{
if (sklb is null || sklb->SkeletonResourceHandle is null)
return null;
@@ -386,6 +387,8 @@ internal unsafe partial record ResolveContext(
node.Children.Add(skpNode);
if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode)
node.Children.Add(phybNode);
+ if (CreateNodeFromKdb(kdbHandle, partialSkeletonIndex) is { } kdbNode)
+ node.Children.Add(kdbNode);
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
return node;
@@ -427,6 +430,24 @@ internal unsafe partial record ResolveContext(
return node;
}
+ private ResourceNode? CreateNodeFromKdb(ResourceHandle* kdbHandle, uint partialSkeletonIndex)
+ {
+ if (kdbHandle is null)
+ return null;
+
+ var path = ResolveKineDriverModulePath(partialSkeletonIndex);
+
+ if (Global.Nodes.TryGetValue((path, (nint)kdbHandle), out var cached))
+ return cached;
+
+ var node = CreateNode(ResourceType.Phyb, 0, kdbHandle, path, false);
+ if (Global.WithUiData)
+ node.FallbackName = "KineDriver Module";
+ Global.Nodes.Add((path, (nint)kdbHandle), node);
+
+ return node;
+ }
+
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
{
var path = gamePath.Path.Split((byte)'/');
diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs
index 3699ae0b..08dee818 100644
--- a/Penumbra/Interop/ResourceTree/ResourceNode.cs
+++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs
@@ -45,7 +45,9 @@ public class ResourceNode : ICloneable
/// Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import).
public bool Protected
- => ForceProtected || Internal || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Pbd;
+ => ForceProtected
+ || Internal
+ || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Skp or ResourceType.Phyb or ResourceType.Kdb or ResourceType.Pbd;
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
{
diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs
index ddef347d..23fe26b8 100644
--- a/Penumbra/Interop/ResourceTree/ResourceTree.cs
+++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs
@@ -121,7 +121,7 @@ public class ResourceTree(
}
}
- AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule);
+ AddSkeleton(Nodes, genericContext, model);
AddMaterialAnimationSkeleton(Nodes, genericContext, model->MaterialAnimationSkeleton);
AddWeapons(globalContext, model);
@@ -178,8 +178,7 @@ public class ResourceTree(
}
}
- AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule,
- $"Weapon #{weaponIndex}, ");
+ AddSkeleton(weaponNodes, genericContext, subObject, $"Weapon #{weaponIndex}, ");
AddMaterialAnimationSkeleton(weaponNodes, genericContext, subObject->MaterialAnimationSkeleton,
$"Weapon #{weaponIndex}, ");
@@ -242,8 +241,11 @@ public class ResourceTree(
}
}
+ private unsafe void AddSkeleton(List nodes, ResolveContext context, CharacterBase* model, string prefix = "")
+ => AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, *(void**)((nint)model + 0x160), prefix);
+
private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics,
- string prefix = "")
+ void* kineDriver, string prefix = "")
{
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
if (eidNode != null)
@@ -259,7 +261,9 @@ public class ResourceTree(
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
{
var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null;
- if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode)
+ // TODO ClientStructs-ify (aers/FFXIVClientStructs#1562)
+ var kdbHandle = kineDriver != null ? *(ResourceHandle**)((nint)kineDriver + 0x20 + 0x18 * i) : null;
+ if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode)
{
if (context.Global.WithUiData)
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs
index 031d24b1..5a29bb6f 100644
--- a/Penumbra/Interop/Structs/StructExtensions.cs
+++ b/Penumbra/Interop/Structs/StructExtensions.cs
@@ -64,6 +64,15 @@ internal static class StructExtensions
return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex));
}
+ public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
+ {
+ // TODO ClientStructs-ify (aers/FFXIVClientStructs#1561)
+ var vf80 = (delegate* unmanaged)((nint*)character.VirtualTable)[80];
+ var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
+ return ToOwnedByteString(vf80((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize,
+ partialSkeletonIndex));
+ }
+
private static unsafe CiByteString ToOwnedByteString(CStringPointer str)
=> str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty;