mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 20:24:17 +01:00
Merge branch 'rt-more-files'
This commit is contained in:
commit
2413424c8a
7 changed files with 311 additions and 145 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit b4a0806e00be4ce8cf3103fd526e4a412b4770b7
|
Subproject commit c59b1da61610e656b3e89f9c33113d08f97ae6c7
|
||||||
|
|
@ -22,6 +22,13 @@ internal partial record ResolveContext
|
||||||
private static bool IsEquipmentSlot(uint slotIndex)
|
private static bool IsEquipmentSlot(uint slotIndex)
|
||||||
=> slotIndex is < 5 or 16 or 17;
|
=> slotIndex is < 5 or 16 or 17;
|
||||||
|
|
||||||
|
private unsafe Variant Variant
|
||||||
|
=> ModelType switch
|
||||||
|
{
|
||||||
|
ModelType.Monster => (byte)((Monster*)CharacterBase)->Variant,
|
||||||
|
_ => Equipment.Variant,
|
||||||
|
};
|
||||||
|
|
||||||
private Utf8GamePath ResolveModelPath()
|
private Utf8GamePath ResolveModelPath()
|
||||||
{
|
{
|
||||||
// Correctness:
|
// Correctness:
|
||||||
|
|
@ -92,7 +99,7 @@ internal partial record ResolveContext
|
||||||
=> ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
=> ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
|
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName),
|
ModelType.Monster => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
|
||||||
_ => ResolveMaterialPathNative(mtrlFileName),
|
_ => ResolveMaterialPathNative(mtrlFileName),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +107,7 @@ internal partial record ResolveContext
|
||||||
[SkipLocalsInit]
|
[SkipLocalsInit]
|
||||||
private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
|
private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
|
||||||
{
|
{
|
||||||
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
|
var variant = ResolveImcData(imc).MaterialId;
|
||||||
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||||
|
|
||||||
Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
|
Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
|
||||||
|
|
@ -118,9 +125,9 @@ internal partial record ResolveContext
|
||||||
return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty;
|
return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty;
|
||||||
|
|
||||||
// Some offhands share materials with the corresponding mainhand
|
// Some offhands share materials with the corresponding mainhand
|
||||||
if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId))
|
if (ItemData.AdaptOffhandImc(Equipment.Set, out var mirroredSetId))
|
||||||
{
|
{
|
||||||
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
|
var variant = ResolveImcData(imc).MaterialId;
|
||||||
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
||||||
|
|
||||||
Span<byte> mirroredFileName = stackalloc byte[32];
|
Span<byte> mirroredFileName = stackalloc byte[32];
|
||||||
|
|
@ -141,31 +148,16 @@ internal partial record ResolveContext
|
||||||
return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName);
|
return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
|
private unsafe ImcEntry ResolveImcData(ResourceHandle* imc)
|
||||||
{
|
|
||||||
var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant);
|
|
||||||
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
|
|
||||||
|
|
||||||
Span<byte> pathBuffer = stackalloc byte[CharaBase.PathBufferSize];
|
|
||||||
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
|
|
||||||
|
|
||||||
return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe byte ResolveMaterialVariant(ResourceHandle* imc, Variant variant)
|
|
||||||
{
|
{
|
||||||
var imcFileData = imc->GetDataSpan();
|
var imcFileData = imc->GetDataSpan();
|
||||||
if (imcFileData.IsEmpty)
|
if (imcFileData.IsEmpty)
|
||||||
{
|
{
|
||||||
Penumbra.Log.Warning($"IMC resource handle with path {imc->FileName.AsByteString()} doesn't have a valid data span");
|
Penumbra.Log.Warning($"IMC resource handle with path {imc->FileName.AsByteString()} doesn't have a valid data span");
|
||||||
return variant.Id;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry = ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), variant, out var exists);
|
return ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), Variant, out _);
|
||||||
if (!exists)
|
|
||||||
return variant.Id;
|
|
||||||
|
|
||||||
return entry.MaterialId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Span<byte> AssembleMaterialPath(Span<byte> materialPathBuffer, ReadOnlySpan<byte> modelPath, byte variant,
|
private static Span<byte> AssembleMaterialPath(Span<byte> materialPathBuffer, ReadOnlySpan<byte> modelPath, byte variant,
|
||||||
|
|
@ -256,7 +248,7 @@ internal partial record ResolveContext
|
||||||
if (faceId < 201)
|
if (faceId < 201)
|
||||||
faceId -= tribe switch
|
faceId -= tribe switch
|
||||||
{
|
{
|
||||||
0xB when modelType == 4 => 100,
|
0xB when modelType is 4 => 100,
|
||||||
0xE | 0xF => 100,
|
0xE | 0xF => 100,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
|
@ -305,7 +297,7 @@ internal partial record ResolveContext
|
||||||
private Utf8GamePath ResolveHumanSkeletonParameterPath(uint partialSkeletonIndex)
|
private Utf8GamePath ResolveHumanSkeletonParameterPath(uint partialSkeletonIndex)
|
||||||
{
|
{
|
||||||
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
|
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
|
||||||
if (set == 0)
|
if (set.Id is 0)
|
||||||
return Utf8GamePath.Empty;
|
return Utf8GamePath.Empty;
|
||||||
|
|
||||||
var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set);
|
var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set);
|
||||||
|
|
@ -317,4 +309,52 @@ internal partial record ResolveContext
|
||||||
var path = CharacterBase->ResolveSkpPathAsByteString(partialSkeletonIndex);
|
var path = CharacterBase->ResolveSkpPathAsByteString(partialSkeletonIndex);
|
||||||
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Utf8GamePath ResolvePhysicsModulePath(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
// Correctness and Safety:
|
||||||
|
// Resolving a physics 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 => ResolveHumanPhysicsModulePath(partialSkeletonIndex),
|
||||||
|
_ => ResolvePhysicsModulePathNative(partialSkeletonIndex),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Utf8GamePath ResolveHumanPhysicsModulePath(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
|
||||||
|
if (set.Id is 0)
|
||||||
|
return Utf8GamePath.Empty;
|
||||||
|
|
||||||
|
var path = GamePaths.Skeleton.Phyb.Path(raceCode, slot, set);
|
||||||
|
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolvePhysicsModulePathNative(uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
var path = CharacterBase->ResolvePhybPathAsByteString(partialSkeletonIndex);
|
||||||
|
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc)
|
||||||
|
{
|
||||||
|
var animation = ResolveImcData(imc).MaterialAnimationId;
|
||||||
|
if (animation is 0)
|
||||||
|
return Utf8GamePath.Empty;
|
||||||
|
|
||||||
|
var path = CharacterBase->ResolveMaterialPapPathAsByteString(SlotIndex, animation);
|
||||||
|
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Utf8GamePath ResolveDecalPath(ResourceHandle* imc)
|
||||||
|
{
|
||||||
|
var decal = ResolveImcData(imc).DecalId;
|
||||||
|
if (decal is 0)
|
||||||
|
return Utf8GamePath.Empty;
|
||||||
|
|
||||||
|
var path = GamePaths.Equipment.Decal.Path(decal);
|
||||||
|
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ internal unsafe partial record ResolveContext(
|
||||||
|
|
||||||
private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath)
|
private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath)
|
||||||
{
|
{
|
||||||
if (resourceHandle == null)
|
if (resourceHandle is null)
|
||||||
return null;
|
return null;
|
||||||
if (gamePath.IsEmpty)
|
if (gamePath.IsEmpty)
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -65,7 +65,7 @@ internal unsafe partial record ResolveContext(
|
||||||
[SkipLocalsInit]
|
[SkipLocalsInit]
|
||||||
private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, CiByteString gamePath, bool dx11)
|
private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, CiByteString gamePath, bool dx11)
|
||||||
{
|
{
|
||||||
if (resourceHandle == null)
|
if (resourceHandle is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
Utf8GamePath path;
|
Utf8GamePath path;
|
||||||
|
|
@ -105,7 +105,7 @@ internal unsafe partial record ResolveContext(
|
||||||
private ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
private ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
||||||
Utf8GamePath gamePath)
|
Utf8GamePath gamePath)
|
||||||
{
|
{
|
||||||
if (resourceHandle == null)
|
if (resourceHandle is null)
|
||||||
throw new ArgumentNullException(nameof(resourceHandle));
|
throw new ArgumentNullException(nameof(resourceHandle));
|
||||||
|
|
||||||
if (Global.Nodes.TryGetValue((gamePath, (nint)resourceHandle), out var cached))
|
if (Global.Nodes.TryGetValue((gamePath, (nint)resourceHandle), out var cached))
|
||||||
|
|
@ -117,7 +117,7 @@ internal unsafe partial record ResolveContext(
|
||||||
private ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
private ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
||||||
Utf8GamePath gamePath, bool autoAdd = true)
|
Utf8GamePath gamePath, bool autoAdd = true)
|
||||||
{
|
{
|
||||||
if (resourceHandle == null)
|
if (resourceHandle is null)
|
||||||
throw new ArgumentNullException(nameof(resourceHandle));
|
throw new ArgumentNullException(nameof(resourceHandle));
|
||||||
|
|
||||||
var fileName = (ReadOnlySpan<byte>)resourceHandle->FileName.AsSpan();
|
var fileName = (ReadOnlySpan<byte>)resourceHandle->FileName.AsSpan();
|
||||||
|
|
@ -141,7 +141,7 @@ internal unsafe partial record ResolveContext(
|
||||||
|
|
||||||
public ResourceNode? CreateNodeFromEid(ResourceHandle* eid)
|
public ResourceNode? CreateNodeFromEid(ResourceHandle* eid)
|
||||||
{
|
{
|
||||||
if (eid == null)
|
if (eid is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!Utf8GamePath.FromByteString(CharacterBase->ResolveEidPathAsByteString(), out var path))
|
if (!Utf8GamePath.FromByteString(CharacterBase->ResolveEidPathAsByteString(), out var path))
|
||||||
|
|
@ -152,7 +152,7 @@ internal unsafe partial record ResolveContext(
|
||||||
|
|
||||||
public ResourceNode? CreateNodeFromImc(ResourceHandle* imc)
|
public ResourceNode? CreateNodeFromImc(ResourceHandle* imc)
|
||||||
{
|
{
|
||||||
if (imc == null)
|
if (imc is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!Utf8GamePath.FromByteString(CharacterBase->ResolveImcPathAsByteString(SlotIndex), out var path))
|
if (!Utf8GamePath.FromByteString(CharacterBase->ResolveImcPathAsByteString(SlotIndex), out var path))
|
||||||
|
|
@ -163,7 +163,7 @@ internal unsafe partial record ResolveContext(
|
||||||
|
|
||||||
public ResourceNode? CreateNodeFromPbd(ResourceHandle* pbd)
|
public ResourceNode? CreateNodeFromPbd(ResourceHandle* pbd)
|
||||||
{
|
{
|
||||||
if (pbd == null)
|
if (pbd is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return GetOrCreateNode(ResourceType.Pbd, 0, pbd, PreBoneDeformerReplacer.PreBoneDeformerPath);
|
return GetOrCreateNode(ResourceType.Pbd, 0, pbd, PreBoneDeformerReplacer.PreBoneDeformerPath);
|
||||||
|
|
@ -171,7 +171,7 @@ internal unsafe partial record ResolveContext(
|
||||||
|
|
||||||
public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath)
|
public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath)
|
||||||
{
|
{
|
||||||
if (tex == null)
|
if (tex is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!Utf8GamePath.FromString(gamePath, out var path))
|
if (!Utf8GamePath.FromString(gamePath, out var path))
|
||||||
|
|
@ -180,9 +180,17 @@ internal unsafe partial record ResolveContext(
|
||||||
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path);
|
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc)
|
public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, Utf8GamePath gamePath)
|
||||||
{
|
{
|
||||||
if (mdl == null || mdl->ModelResourceHandle == null)
|
if (tex is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle)
|
||||||
|
{
|
||||||
|
if (mdl is null || mdl->ModelResourceHandle is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var mdlResource = mdl->ModelResourceHandle;
|
var mdlResource = mdl->ModelResourceHandle;
|
||||||
|
|
@ -197,12 +205,12 @@ internal unsafe partial record ResolveContext(
|
||||||
for (var i = 0; i < mdl->MaterialCount; i++)
|
for (var i = 0; i < mdl->MaterialCount; i++)
|
||||||
{
|
{
|
||||||
var mtrl = mdl->Materials[i];
|
var mtrl = mdl->Materials[i];
|
||||||
if (mtrl == null)
|
if (mtrl is null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var mtrlFileName = mdlResource->GetMaterialFileNameBySlot((uint)i);
|
var mtrlFileName = mdlResource->GetMaterialFileNameBySlot((uint)i);
|
||||||
var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imc, mtrlFileName));
|
var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imc, mtrlFileName));
|
||||||
if (mtrlNode != null)
|
if (mtrlNode is not null)
|
||||||
{
|
{
|
||||||
if (Global.WithUiData)
|
if (Global.WithUiData)
|
||||||
mtrlNode.FallbackName = $"Material #{i}";
|
mtrlNode.FallbackName = $"Material #{i}";
|
||||||
|
|
@ -210,6 +218,12 @@ internal unsafe partial record ResolveContext(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode)
|
||||||
|
node.Children.Add(decalNode);
|
||||||
|
|
||||||
|
if (CreateNodeFromMaterialPap(mpapHandle, imc) is { } mpapNode)
|
||||||
|
node.Children.Add(mpapNode);
|
||||||
|
|
||||||
Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node);
|
Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
|
|
@ -217,7 +231,7 @@ internal unsafe partial record ResolveContext(
|
||||||
|
|
||||||
private ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path)
|
private ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path)
|
||||||
{
|
{
|
||||||
if (mtrl == null || mtrl->MaterialResourceHandle == null)
|
if (mtrl is null || mtrl->MaterialResourceHandle is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var resource = mtrl->MaterialResourceHandle;
|
var resource = mtrl->MaterialResourceHandle;
|
||||||
|
|
@ -226,15 +240,15 @@ internal unsafe partial record ResolveContext(
|
||||||
|
|
||||||
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false);
|
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false);
|
||||||
var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName));
|
var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName));
|
||||||
if (shpkNode != null)
|
if (shpkNode is not null)
|
||||||
{
|
{
|
||||||
if (Global.WithUiData)
|
if (Global.WithUiData)
|
||||||
shpkNode.Name = "Shader Package";
|
shpkNode.Name = "Shader Package";
|
||||||
node.Children.Add(shpkNode);
|
node.Children.Add(shpkNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
var shpkNames = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null;
|
var shpkNames = Global.WithUiData && shpkNode is not null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null;
|
||||||
var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
|
var shpk = Global.WithUiData && shpkNode is not null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
|
||||||
|
|
||||||
var alreadyProcessedSamplerIds = new HashSet<uint>();
|
var alreadyProcessedSamplerIds = new HashSet<uint>();
|
||||||
for (var i = 0; i < resource->TextureCount; i++)
|
for (var i = 0; i < resource->TextureCount; i++)
|
||||||
|
|
@ -247,7 +261,7 @@ internal unsafe partial record ResolveContext(
|
||||||
if (Global.WithUiData)
|
if (Global.WithUiData)
|
||||||
{
|
{
|
||||||
string? name = null;
|
string? name = null;
|
||||||
if (shpk != null)
|
if (shpk is not null)
|
||||||
{
|
{
|
||||||
var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds);
|
var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds);
|
||||||
var samplerId = index != 0x001F
|
var samplerId = index != 0x001F
|
||||||
|
|
@ -301,9 +315,61 @@ internal unsafe partial record ResolveContext(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
|
public ResourceNode? CreateNodeFromDecal(TextureResourceHandle* decalHandle, ResourceHandle* imc)
|
||||||
{
|
{
|
||||||
if (sklb == null || sklb->SkeletonResourceHandle == null)
|
if (decalHandle is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var path = ResolveDecalPath(imc);
|
||||||
|
if (path.IsEmpty)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var node = CreateNodeFromTex(decalHandle, path)!;
|
||||||
|
if (Global.WithUiData)
|
||||||
|
node.FallbackName = "Decal";
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceNode? CreateNodeFromMaterialPap(ResourceHandle* mpapHandle, ResourceHandle* imc)
|
||||||
|
{
|
||||||
|
if (mpapHandle is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var path = ResolveMaterialAnimationPath(imc);
|
||||||
|
if (path.IsEmpty)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (Global.Nodes.TryGetValue((path, (nint)mpapHandle), out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var node = CreateNode(ResourceType.Pap, 0, mpapHandle, path);
|
||||||
|
if (Global.WithUiData)
|
||||||
|
node.FallbackName = "Material Animation";
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceNode? CreateNodeFromMaterialSklb(SkeletonResourceHandle* sklbHandle)
|
||||||
|
{
|
||||||
|
if (sklbHandle is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!Utf8GamePath.FromString(GamePaths.Skeleton.Sklb.MaterialAnimationSkeletonPath, out var path))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (Global.Nodes.TryGetValue((path, (nint)sklbHandle), out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var node = CreateNode(ResourceType.Sklb, 0, (ResourceHandle*)sklbHandle, path);
|
||||||
|
node.ForceInternal = true;
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
if (sklb is null || sklb->SkeletonResourceHandle is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var path = ResolveSkeletonPath(partialSkeletonIndex);
|
var path = ResolveSkeletonPath(partialSkeletonIndex);
|
||||||
|
|
@ -312,9 +378,10 @@ internal unsafe partial record ResolveContext(
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false);
|
var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false);
|
||||||
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex);
|
if (CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex) is { } skpNode)
|
||||||
if (skpNode != null)
|
|
||||||
node.Children.Add(skpNode);
|
node.Children.Add(skpNode);
|
||||||
|
if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode)
|
||||||
|
node.Children.Add(phybNode);
|
||||||
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
|
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
|
|
@ -322,7 +389,7 @@ internal unsafe partial record ResolveContext(
|
||||||
|
|
||||||
private ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
|
private ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
|
||||||
{
|
{
|
||||||
if (sklb == null || sklb->SkeletonParameterResourceHandle == null)
|
if (sklb is null || sklb->SkeletonParameterResourceHandle is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var path = ResolveSkeletonParameterPath(partialSkeletonIndex);
|
var path = ResolveSkeletonParameterPath(partialSkeletonIndex);
|
||||||
|
|
@ -338,11 +405,31 @@ internal unsafe partial record ResolveContext(
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ResourceNode? CreateNodeFromPhyb(ResourceHandle* phybHandle, uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
if (phybHandle is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var path = ResolvePhysicsModulePath(partialSkeletonIndex);
|
||||||
|
|
||||||
|
if (Global.Nodes.TryGetValue((path, (nint)phybHandle), out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var node = CreateNode(ResourceType.Phyb, 0, phybHandle, path, false);
|
||||||
|
if (Global.WithUiData)
|
||||||
|
node.FallbackName = "Physics Module";
|
||||||
|
Global.Nodes.Add((path, (nint)phybHandle), node);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
|
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
|
||||||
{
|
{
|
||||||
var path = gamePath.Path.Split((byte)'/');
|
var path = gamePath.Path.Split((byte)'/');
|
||||||
// Weapons intentionally left out.
|
// Weapons intentionally left out.
|
||||||
var isEquipment = path.Count >= 2 && path[0].Span.SequenceEqual("chara"u8) && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8));
|
var isEquipment = path.Count >= 2
|
||||||
|
&& path[0].Span.SequenceEqual("chara"u8)
|
||||||
|
&& (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8));
|
||||||
if (isEquipment)
|
if (isEquipment)
|
||||||
foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot()))
|
foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot()))
|
||||||
{
|
{
|
||||||
|
|
@ -358,7 +445,7 @@ internal unsafe partial record ResolveContext(
|
||||||
}
|
}
|
||||||
|
|
||||||
var dataFromPath = GuessUiDataFromPath(gamePath);
|
var dataFromPath = GuessUiDataFromPath(gamePath);
|
||||||
if (dataFromPath.Name != null)
|
if (dataFromPath.Name is not null)
|
||||||
return dataFromPath;
|
return dataFromPath;
|
||||||
|
|
||||||
return isEquipment
|
return isEquipment
|
||||||
|
|
@ -373,24 +460,13 @@ internal unsafe partial record ResolveContext(
|
||||||
var name = obj.Key;
|
var name = obj.Key;
|
||||||
if (obj.Value is IdentifiedCustomization)
|
if (obj.Value is IdentifiedCustomization)
|
||||||
name = name[14..].Trim();
|
name = name[14..].Trim();
|
||||||
if (name != "Unknown")
|
if (name is not "Unknown")
|
||||||
return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag());
|
return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown);
|
return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? SafeGet(ReadOnlySpan<string> array, Index index)
|
|
||||||
{
|
|
||||||
var i = index.GetOffset(array.Length);
|
|
||||||
return i >= 0 && i < array.Length ? array[i] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ulong GetResourceHandleLength(ResourceHandle* handle)
|
private static ulong GetResourceHandleLength(ResourceHandle* handle)
|
||||||
{
|
=> handle is null ? 0ul : handle->GetLength();
|
||||||
if (handle == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return handle->GetLength();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ public class ResourceNode : ICloneable
|
||||||
public Utf8GamePath[] PossibleGamePaths;
|
public Utf8GamePath[] PossibleGamePaths;
|
||||||
public FullPath FullPath;
|
public FullPath FullPath;
|
||||||
public PathStatus FullPathStatus;
|
public PathStatus FullPathStatus;
|
||||||
|
public bool ForceInternal;
|
||||||
|
public bool ForceProtected;
|
||||||
public string? ModName;
|
public string? ModName;
|
||||||
public readonly WeakReference<Mod> Mod = new(null!);
|
public readonly WeakReference<Mod> Mod = new(null!);
|
||||||
public string? ModRelativePath;
|
public string? ModRelativePath;
|
||||||
|
|
@ -37,8 +39,13 @@ public class ResourceNode : ICloneable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Whether to treat the file as internal (hide from user unless debug mode is on). </summary>
|
||||||
public bool Internal
|
public bool Internal
|
||||||
=> Type is ResourceType.Eid or ResourceType.Imc;
|
=> ForceInternal || Type is ResourceType.Eid or ResourceType.Imc;
|
||||||
|
|
||||||
|
/// <summary> Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). </summary>
|
||||||
|
public bool Protected
|
||||||
|
=> ForceProtected || Internal || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Pbd;
|
||||||
|
|
||||||
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
|
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
|
||||||
{
|
{
|
||||||
|
|
@ -67,6 +74,8 @@ public class ResourceNode : ICloneable
|
||||||
Mod = other.Mod;
|
Mod = other.Mod;
|
||||||
ModRelativePath = other.ModRelativePath;
|
ModRelativePath = other.ModRelativePath;
|
||||||
AdditionalData = other.AdditionalData;
|
AdditionalData = other.AdditionalData;
|
||||||
|
ForceInternal = other.ForceInternal;
|
||||||
|
ForceProtected = other.ForceProtected;
|
||||||
Length = other.Length;
|
Length = other.Length;
|
||||||
Children = other.Children;
|
Children = other.Children;
|
||||||
ResolveContext = other.ResolveContext;
|
ResolveContext = other.ResolveContext;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Physics;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
using FFXIVClientStructs.Interop;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
@ -10,45 +12,39 @@ using Penumbra.UI;
|
||||||
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
|
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
|
||||||
using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex;
|
using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex;
|
||||||
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
|
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
|
||||||
|
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
|
||||||
|
|
||||||
namespace Penumbra.Interop.ResourceTree;
|
namespace Penumbra.Interop.ResourceTree;
|
||||||
|
|
||||||
public class ResourceTree
|
public class ResourceTree(
|
||||||
|
string name,
|
||||||
|
string anonymizedName,
|
||||||
|
int gameObjectIndex,
|
||||||
|
nint gameObjectAddress,
|
||||||
|
nint drawObjectAddress,
|
||||||
|
bool localPlayerRelated,
|
||||||
|
bool playerRelated,
|
||||||
|
bool networked,
|
||||||
|
string collectionName,
|
||||||
|
string anonymizedCollectionName)
|
||||||
{
|
{
|
||||||
public readonly string Name;
|
public readonly string Name = name;
|
||||||
public readonly string AnonymizedName;
|
public readonly string AnonymizedName = anonymizedName;
|
||||||
public readonly int GameObjectIndex;
|
public readonly int GameObjectIndex = gameObjectIndex;
|
||||||
public readonly nint GameObjectAddress;
|
public readonly nint GameObjectAddress = gameObjectAddress;
|
||||||
public readonly nint DrawObjectAddress;
|
public readonly nint DrawObjectAddress = drawObjectAddress;
|
||||||
public readonly bool LocalPlayerRelated;
|
public readonly bool LocalPlayerRelated = localPlayerRelated;
|
||||||
public readonly bool PlayerRelated;
|
public readonly bool PlayerRelated = playerRelated;
|
||||||
public readonly bool Networked;
|
public readonly bool Networked = networked;
|
||||||
public readonly string CollectionName;
|
public readonly string CollectionName = collectionName;
|
||||||
public readonly string AnonymizedCollectionName;
|
public readonly string AnonymizedCollectionName = anonymizedCollectionName;
|
||||||
public readonly List<ResourceNode> Nodes;
|
public readonly List<ResourceNode> Nodes = [];
|
||||||
public readonly HashSet<ResourceNode> FlatNodes;
|
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, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress,
|
|
||||||
bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
AnonymizedName = anonymizedName;
|
|
||||||
GameObjectIndex = gameObjectIndex;
|
|
||||||
GameObjectAddress = gameObjectAddress;
|
|
||||||
DrawObjectAddress = drawObjectAddress;
|
|
||||||
LocalPlayerRelated = localPlayerRelated;
|
|
||||||
Networked = networked;
|
|
||||||
PlayerRelated = playerRelated;
|
|
||||||
CollectionName = collectionName;
|
|
||||||
AnonymizedCollectionName = anonymizedCollectionName;
|
|
||||||
Nodes = [];
|
|
||||||
FlatNodes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ProcessPostfix(Action<ResourceNode, ResourceNode?> action)
|
public void ProcessPostfix(Action<ResourceNode, ResourceNode?> action)
|
||||||
{
|
{
|
||||||
foreach (var node in Nodes)
|
foreach (var node in Nodes)
|
||||||
|
|
@ -70,10 +66,22 @@ public class ResourceTree
|
||||||
};
|
};
|
||||||
ModelId = character->ModelContainer.ModelCharaId;
|
ModelId = character->ModelContainer.ModelCharaId;
|
||||||
CustomizeData = character->DrawData.CustomizeData;
|
CustomizeData = character->DrawData.CustomizeData;
|
||||||
RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;
|
RaceCode = human is not null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;
|
||||||
|
|
||||||
var genericContext = globalContext.CreateContext(model);
|
var genericContext = globalContext.CreateContext(model);
|
||||||
|
|
||||||
|
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||||
|
var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948);
|
||||||
|
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, model->SlotCount) : [];
|
||||||
|
var decalArray = modelType switch
|
||||||
|
{
|
||||||
|
ModelType.Human => human->SlotDecalsSpan,
|
||||||
|
ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals,
|
||||||
|
ModelType.Weapon => [((Weapon*)model)->Decal],
|
||||||
|
ModelType.Monster => [((Monster*)model)->Decal],
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
|
||||||
for (var i = 0u; i < model->SlotCount; ++i)
|
for (var i = 0u; i < model->SlotCount; ++i)
|
||||||
{
|
{
|
||||||
var slotContext = modelType switch
|
var slotContext = modelType switch
|
||||||
|
|
@ -91,8 +99,7 @@ public class ResourceTree
|
||||||
};
|
};
|
||||||
|
|
||||||
var imc = (ResourceHandle*)model->IMCArray[i];
|
var imc = (ResourceHandle*)model->IMCArray[i];
|
||||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
if (slotContext.CreateNodeFromImc(imc) is { } imcNode)
|
||||||
if (imcNode != null)
|
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
imcNode.FallbackName = $"IMC #{i}";
|
imcNode.FallbackName = $"IMC #{i}";
|
||||||
|
|
@ -100,8 +107,8 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
|
|
||||||
var mdl = model->Models[i];
|
var mdl = model->Models[i];
|
||||||
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
|
if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null,
|
||||||
if (mdlNode != null)
|
i < mpapArray.Length ? mpapArray[(int)i].Value : null) is { } mdlNode)
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
mdlNode.FallbackName = $"Model #{i}";
|
mdlNode.FallbackName = $"Model #{i}";
|
||||||
|
|
@ -109,11 +116,13 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton);
|
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule);
|
||||||
|
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||||
|
AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940));
|
||||||
|
|
||||||
AddWeapons(globalContext, model);
|
AddWeapons(globalContext, model);
|
||||||
|
|
||||||
if (human != null)
|
if (human is not null)
|
||||||
AddHumanResources(globalContext, human);
|
AddHumanResources(globalContext, human);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,12 +132,12 @@ public class ResourceTree
|
||||||
var weaponNodes = new List<ResourceNode>();
|
var weaponNodes = new List<ResourceNode>();
|
||||||
foreach (var baseSubObject in model->DrawObject.Object.ChildObjects)
|
foreach (var baseSubObject in model->DrawObject.Object.ChildObjects)
|
||||||
{
|
{
|
||||||
if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase)
|
if (baseSubObject->GetObjectType() is not FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var subObject = (CharacterBase*)baseSubObject;
|
var subObject = (CharacterBase*)baseSubObject;
|
||||||
|
|
||||||
if (subObject->GetModelType() != ModelType.Weapon)
|
if (subObject->GetModelType() is not ModelType.Weapon)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var weapon = (Weapon*)subObject;
|
var weapon = (Weapon*)subObject;
|
||||||
|
|
@ -140,13 +149,16 @@ public class ResourceTree
|
||||||
|
|
||||||
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
|
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
|
||||||
|
|
||||||
|
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||||
|
var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948);
|
||||||
|
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, subObject->SlotCount) : [];
|
||||||
|
|
||||||
for (var i = 0; i < subObject->SlotCount; ++i)
|
for (var i = 0; i < subObject->SlotCount; ++i)
|
||||||
{
|
{
|
||||||
var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType);
|
var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType);
|
||||||
|
|
||||||
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
||||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
if (slotContext.CreateNodeFromImc(imc) is { } imcNode)
|
||||||
if (imcNode != null)
|
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}";
|
imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}";
|
||||||
|
|
@ -154,8 +166,7 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
|
|
||||||
var mdl = subObject->Models[i];
|
var mdl = subObject->Models[i];
|
||||||
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
|
if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode)
|
||||||
if (mdlNode != null)
|
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
||||||
|
|
@ -163,7 +174,11 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, ");
|
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule,
|
||||||
|
$"Weapon #{weaponIndex}, ");
|
||||||
|
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||||
|
AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940),
|
||||||
|
$"Weapon #{weaponIndex}, ");
|
||||||
|
|
||||||
++weaponIndex;
|
++weaponIndex;
|
||||||
}
|
}
|
||||||
|
|
@ -176,10 +191,9 @@ public class ResourceTree
|
||||||
var genericContext = globalContext.CreateContext(&human->CharacterBase);
|
var genericContext = globalContext.CreateContext(&human->CharacterBase);
|
||||||
|
|
||||||
var cache = globalContext.Collection._cache;
|
var cache = globalContext.Collection._cache;
|
||||||
if (cache != null && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle))
|
if (cache is not null
|
||||||
{
|
&& cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)
|
||||||
var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle);
|
&& genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle) is { } pbdNode)
|
||||||
if (pbdNode != null)
|
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
{
|
{
|
||||||
|
|
@ -190,14 +204,12 @@ public class ResourceTree
|
||||||
|
|
||||||
Nodes.Add(pbdNode);
|
Nodes.Add(pbdNode);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F);
|
var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F);
|
||||||
var decalPath = decalId != 0
|
var decalPath = decalId is not 0
|
||||||
? GamePaths.Human.Decal.FaceDecalPath(decalId)
|
? GamePaths.Human.Decal.FaceDecalPath(decalId)
|
||||||
: GamePaths.Tex.TransparentPath;
|
: GamePaths.Tex.TransparentPath;
|
||||||
var decalNode = genericContext.CreateNodeFromTex(human->Decal, decalPath);
|
if (genericContext.CreateNodeFromTex(human->Decal, decalPath) is { } decalNode)
|
||||||
if (decalNode != null)
|
|
||||||
{
|
{
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
{
|
{
|
||||||
|
|
@ -213,9 +225,9 @@ public class ResourceTree
|
||||||
var legacyDecalPath = hasLegacyDecal
|
var legacyDecalPath = hasLegacyDecal
|
||||||
? GamePaths.Human.Decal.LegacyDecalPath
|
? GamePaths.Human.Decal.LegacyDecalPath
|
||||||
: GamePaths.Tex.TransparentPath;
|
: GamePaths.Tex.TransparentPath;
|
||||||
var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath);
|
if (genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath) is { } legacyDecalNode)
|
||||||
if (legacyDecalNode != null)
|
|
||||||
{
|
{
|
||||||
|
legacyDecalNode.ForceProtected = !hasLegacyDecal;
|
||||||
if (globalContext.WithUiData)
|
if (globalContext.WithUiData)
|
||||||
{
|
{
|
||||||
legacyDecalNode = legacyDecalNode.Clone();
|
legacyDecalNode = legacyDecalNode.Clone();
|
||||||
|
|
@ -227,7 +239,8 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, string prefix = "")
|
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics,
|
||||||
|
string prefix = "")
|
||||||
{
|
{
|
||||||
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
|
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
|
||||||
if (eidNode != null)
|
if (eidNode != null)
|
||||||
|
|
@ -242,8 +255,9 @@ public class ResourceTree
|
||||||
|
|
||||||
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
||||||
{
|
{
|
||||||
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], (uint)i);
|
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||||
if (sklbNode != null)
|
var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null;
|
||||||
|
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode)
|
||||||
{
|
{
|
||||||
if (context.Global.WithUiData)
|
if (context.Global.WithUiData)
|
||||||
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
|
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
|
||||||
|
|
@ -251,4 +265,16 @@ public class ResourceTree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe void AddMaterialAnimationSkeleton(List<ResourceNode> nodes, ResolveContext context, SkeletonResourceHandle* sklbHandle,
|
||||||
|
string prefix = "")
|
||||||
|
{
|
||||||
|
var sklbNode = context.CreateNodeFromMaterialSklb(sklbHandle);
|
||||||
|
if (sklbNode is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (context.Global.WithUiData)
|
||||||
|
sklbNode.FallbackName = $"{prefix}Material Animation Skeleton";
|
||||||
|
nodes.Add(sklbNode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ internal static class StructExtensions
|
||||||
return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName));
|
return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId)
|
||||||
|
{
|
||||||
|
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
|
return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId));
|
||||||
|
}
|
||||||
|
|
||||||
public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||||
{
|
{
|
||||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
|
|
@ -45,6 +51,12 @@ internal static class StructExtensions
|
||||||
return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex));
|
return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
|
return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex));
|
||||||
|
}
|
||||||
|
|
||||||
private static unsafe CiByteString ToOwnedByteString(byte* str)
|
private static unsafe CiByteString ToOwnedByteString(byte* str)
|
||||||
=> str == null ? CiByteString.Empty : new CiByteString(str).Clone();
|
=> str == null ? CiByteString.Empty : new CiByteString(str).Clone();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Lumina.Data;
|
using Lumina.Data;
|
||||||
using OtterGui;
|
using OtterGui.Text;
|
||||||
using OtterGui.Raii;
|
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Interop.ResourceTree;
|
using Penumbra.Interop.ResourceTree;
|
||||||
|
|
@ -43,7 +42,7 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
private void DrawQuickImportTab()
|
private void DrawQuickImportTab()
|
||||||
{
|
{
|
||||||
using var tab = ImRaii.TabItem("Import from Screen");
|
using var tab = ImUtf8.TabItem("Import from Screen"u8);
|
||||||
if (!tab)
|
if (!tab)
|
||||||
{
|
{
|
||||||
_quickImportActions.Clear();
|
_quickImportActions.Clear();
|
||||||
|
|
@ -73,14 +72,14 @@ public partial class ModEditWindow
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var file = _gameData.GetFile(path);
|
var file = _gameData.GetFile(path);
|
||||||
writable = file == null ? null : new RawGameFileWritable(file);
|
writable = file is null ? null : new RawGameFileWritable(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
_quickImportWritables.Add(resourceNode.FullPath, writable);
|
_quickImportWritables.Add(resourceNode.FullPath, writable);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.",
|
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize,
|
||||||
resourceNode.FullPath.FullName.Length == 0 || writable == null, true))
|
resourceNode.FullPath.FullName.Length is 0 || writable is null))
|
||||||
{
|
{
|
||||||
var fullPathStr = resourceNode.FullPath.FullName;
|
var fullPathStr = resourceNode.FullPath.FullName;
|
||||||
var ext = resourceNode.PossibleGamePaths.Length == 1
|
var ext = resourceNode.PossibleGamePaths.Length == 1
|
||||||
|
|
@ -110,15 +109,19 @@ public partial class ModEditWindow
|
||||||
_quickImportActions.Add((resourceNode.GamePath, writable), quickImport);
|
_quickImportActions.Add((resourceNode.GamePath, writable), quickImport);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize,
|
var canQuickImport = quickImport.CanExecute;
|
||||||
$"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true))
|
var quickImportEnabled = canQuickImport && (!resourceNode.Protected || _config.DeleteModModifier.IsActive());
|
||||||
|
if (ImUtf8.IconButton(FontAwesomeIcon.FileImport,
|
||||||
|
$"Add a copy of this file to {quickImport.OptionName}.{(canQuickImport && !quickImportEnabled ? $"\nHold {_config.DeleteModModifier} while clicking to add." : string.Empty)}",
|
||||||
|
buttonSize,
|
||||||
|
!quickImportEnabled))
|
||||||
{
|
{
|
||||||
quickImport.Execute();
|
quickImport.Execute();
|
||||||
_quickImportActions.Remove((resourceNode.GamePath, writable));
|
_quickImportActions.Remove((resourceNode.GamePath, writable));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record class RawFileWritable(string Path) : IWritable
|
private record RawFileWritable(string Path) : IWritable
|
||||||
{
|
{
|
||||||
public bool Valid
|
public bool Valid
|
||||||
=> true;
|
=> true;
|
||||||
|
|
@ -127,7 +130,7 @@ public partial class ModEditWindow
|
||||||
=> File.ReadAllBytes(Path);
|
=> File.ReadAllBytes(Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record class RawGameFileWritable(FileResource FileResource) : IWritable
|
private record RawGameFileWritable(FileResource FileResource) : IWritable
|
||||||
{
|
{
|
||||||
public bool Valid
|
public bool Valid
|
||||||
=> true;
|
=> true;
|
||||||
|
|
@ -185,19 +188,19 @@ public partial class ModEditWindow
|
||||||
public static QuickImportAction Prepare(ModEditWindow owner, Utf8GamePath gamePath, IWritable? file)
|
public static QuickImportAction Prepare(ModEditWindow owner, Utf8GamePath gamePath, IWritable? file)
|
||||||
{
|
{
|
||||||
var editor = owner._editor;
|
var editor = owner._editor;
|
||||||
if (editor == null)
|
if (editor is null)
|
||||||
return new QuickImportAction(owner._editor, FallbackOptionName, gamePath);
|
return new QuickImportAction(owner._editor, FallbackOptionName, gamePath);
|
||||||
|
|
||||||
var subMod = editor.Option!;
|
var subMod = editor.Option!;
|
||||||
var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName;
|
var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName;
|
||||||
if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes)
|
if (gamePath.IsEmpty || file is null || editor.FileEditor.Changes)
|
||||||
return new QuickImportAction(editor, optionName, gamePath);
|
return new QuickImportAction(editor, optionName, gamePath);
|
||||||
|
|
||||||
if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath))
|
if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath))
|
||||||
return new QuickImportAction(editor, optionName, gamePath);
|
return new QuickImportAction(editor, optionName, gamePath);
|
||||||
|
|
||||||
var mod = owner.Mod;
|
var mod = owner.Mod;
|
||||||
if (mod == null)
|
if (mod is null)
|
||||||
return new QuickImportAction(editor, optionName, gamePath);
|
return new QuickImportAction(editor, optionName, gamePath);
|
||||||
|
|
||||||
var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport);
|
var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport);
|
||||||
|
|
@ -232,7 +235,7 @@ public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
var path = mod.ModPath;
|
var path = mod.ModPath;
|
||||||
var subDirs = 0;
|
var subDirs = 0;
|
||||||
if (subMod == null)
|
if (subMod is null)
|
||||||
return (path, subDirs);
|
return (path, subDirs);
|
||||||
|
|
||||||
var name = subMod.Name;
|
var name = subMod.Name;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue