Merge branch 'rt-more-files'

This commit is contained in:
Ottermandias 2025-02-27 05:51:43 +01:00
commit 2413424c8a
7 changed files with 311 additions and 145 deletions

@ -1 +1 @@
Subproject commit b4a0806e00be4ce8cf3103fd526e4a412b4770b7
Subproject commit c59b1da61610e656b3e89f9c33113d08f97ae6c7

View file

@ -22,6 +22,13 @@ internal partial record ResolveContext
private static bool IsEquipmentSlot(uint slotIndex)
=> slotIndex is < 5 or 16 or 17;
private unsafe Variant Variant
=> ModelType switch
{
ModelType.Monster => (byte)((Monster*)CharacterBase)->Variant,
_ => Equipment.Variant,
};
private Utf8GamePath ResolveModelPath()
{
// Correctness:
@ -92,7 +99,7 @@ internal partial record ResolveContext
=> ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName),
ModelType.Monster => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
_ => ResolveMaterialPathNative(mtrlFileName),
};
}
@ -100,7 +107,7 @@ internal partial record ResolveContext
[SkipLocalsInit]
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);
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;
// 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);
Span<byte> mirroredFileName = stackalloc byte[32];
@ -141,31 +148,16 @@ internal partial record ResolveContext
return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName);
}
private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
{
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)
private unsafe ImcEntry ResolveImcData(ResourceHandle* imc)
{
var imcFileData = imc->GetDataSpan();
if (imcFileData.IsEmpty)
{
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);
if (!exists)
return variant.Id;
return entry.MaterialId;
return ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), Variant, out _);
}
private static Span<byte> AssembleMaterialPath(Span<byte> materialPathBuffer, ReadOnlySpan<byte> modelPath, byte variant,
@ -256,7 +248,7 @@ internal partial record ResolveContext
if (faceId < 201)
faceId -= tribe switch
{
0xB when modelType == 4 => 100,
0xB when modelType is 4 => 100,
0xE | 0xF => 100,
_ => 0,
};
@ -305,7 +297,7 @@ internal partial record ResolveContext
private Utf8GamePath ResolveHumanSkeletonParameterPath(uint partialSkeletonIndex)
{
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
if (set == 0)
if (set.Id is 0)
return Utf8GamePath.Empty;
var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set);
@ -317,4 +309,52 @@ internal partial record ResolveContext
var path = CharacterBase->ResolveSkpPathAsByteString(partialSkeletonIndex);
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;
}
}

View file

@ -52,7 +52,7 @@ internal unsafe partial record ResolveContext(
private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath)
{
if (resourceHandle == null)
if (resourceHandle is null)
return null;
if (gamePath.IsEmpty)
return null;
@ -65,7 +65,7 @@ internal unsafe partial record ResolveContext(
[SkipLocalsInit]
private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, CiByteString gamePath, bool dx11)
{
if (resourceHandle == null)
if (resourceHandle is null)
return null;
Utf8GamePath path;
@ -105,7 +105,7 @@ internal unsafe partial record ResolveContext(
private ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
Utf8GamePath gamePath)
{
if (resourceHandle == null)
if (resourceHandle is null)
throw new ArgumentNullException(nameof(resourceHandle));
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,
Utf8GamePath gamePath, bool autoAdd = true)
{
if (resourceHandle == null)
if (resourceHandle is null)
throw new ArgumentNullException(nameof(resourceHandle));
var fileName = (ReadOnlySpan<byte>)resourceHandle->FileName.AsSpan();
@ -141,7 +141,7 @@ internal unsafe partial record ResolveContext(
public ResourceNode? CreateNodeFromEid(ResourceHandle* eid)
{
if (eid == null)
if (eid is null)
return null;
if (!Utf8GamePath.FromByteString(CharacterBase->ResolveEidPathAsByteString(), out var path))
@ -152,7 +152,7 @@ internal unsafe partial record ResolveContext(
public ResourceNode? CreateNodeFromImc(ResourceHandle* imc)
{
if (imc == null)
if (imc is null)
return null;
if (!Utf8GamePath.FromByteString(CharacterBase->ResolveImcPathAsByteString(SlotIndex), out var path))
@ -163,7 +163,7 @@ internal unsafe partial record ResolveContext(
public ResourceNode? CreateNodeFromPbd(ResourceHandle* pbd)
{
if (pbd == null)
if (pbd is null)
return null;
return GetOrCreateNode(ResourceType.Pbd, 0, pbd, PreBoneDeformerReplacer.PreBoneDeformerPath);
@ -171,7 +171,7 @@ internal unsafe partial record ResolveContext(
public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath)
{
if (tex == null)
if (tex is null)
return null;
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);
}
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;
var mdlResource = mdl->ModelResourceHandle;
@ -197,12 +205,12 @@ internal unsafe partial record ResolveContext(
for (var i = 0; i < mdl->MaterialCount; i++)
{
var mtrl = mdl->Materials[i];
if (mtrl == null)
if (mtrl is null)
continue;
var mtrlFileName = mdlResource->GetMaterialFileNameBySlot((uint)i);
var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imc, mtrlFileName));
if (mtrlNode != null)
if (mtrlNode is not null)
{
if (Global.WithUiData)
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);
return node;
@ -217,7 +231,7 @@ internal unsafe partial record ResolveContext(
private ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path)
{
if (mtrl == null || mtrl->MaterialResourceHandle == null)
if (mtrl is null || mtrl->MaterialResourceHandle is null)
return null;
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 shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName));
if (shpkNode != null)
if (shpkNode is not null)
{
if (Global.WithUiData)
shpkNode.Name = "Shader Package";
node.Children.Add(shpkNode);
}
var shpkNames = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null;
var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
var shpkNames = Global.WithUiData && shpkNode is not null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null;
var shpk = Global.WithUiData && shpkNode is not null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
var alreadyProcessedSamplerIds = new HashSet<uint>();
for (var i = 0; i < resource->TextureCount; i++)
@ -247,7 +261,7 @@ internal unsafe partial record ResolveContext(
if (Global.WithUiData)
{
string? name = null;
if (shpk != null)
if (shpk is not null)
{
var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds);
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;
var path = ResolveSkeletonPath(partialSkeletonIndex);
@ -311,10 +377,11 @@ internal unsafe partial record ResolveContext(
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached))
return cached;
var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false);
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex);
if (skpNode != null)
var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false);
if (CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex) is { } skpNode)
node.Children.Add(skpNode);
if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode)
node.Children.Add(phybNode);
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
return node;
@ -322,7 +389,7 @@ internal unsafe partial record ResolveContext(
private ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
{
if (sklb == null || sklb->SkeletonParameterResourceHandle == null)
if (sklb is null || sklb->SkeletonParameterResourceHandle is null)
return null;
var path = ResolveSkeletonParameterPath(partialSkeletonIndex);
@ -338,11 +405,31 @@ internal unsafe partial record ResolveContext(
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)
{
var path = gamePath.Path.Split((byte)'/');
// 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)
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);
if (dataFromPath.Name != null)
if (dataFromPath.Name is not null)
return dataFromPath;
return isEquipment
@ -373,24 +460,13 @@ internal unsafe partial record ResolveContext(
var name = obj.Key;
if (obj.Value is IdentifiedCustomization)
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(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)
{
if (handle == null)
return 0;
return handle->GetLength();
}
=> handle is null ? 0ul : handle->GetLength();
}

View file

@ -17,6 +17,8 @@ public class ResourceNode : ICloneable
public Utf8GamePath[] PossibleGamePaths;
public FullPath FullPath;
public PathStatus FullPathStatus;
public bool ForceInternal;
public bool ForceProtected;
public string? ModName;
public readonly WeakReference<Mod> Mod = new(null!);
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
=> 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)
{
@ -67,6 +74,8 @@ public class ResourceNode : ICloneable
Mod = other.Mod;
ModRelativePath = other.ModRelativePath;
AdditionalData = other.AdditionalData;
ForceInternal = other.ForceInternal;
ForceProtected = other.ForceProtected;
Length = other.Length;
Children = other.Children;
ResolveContext = other.ResolveContext;

View file

@ -1,7 +1,9 @@
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Physics;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -10,45 +12,39 @@ using Penumbra.UI;
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex;
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
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 AnonymizedName;
public readonly int GameObjectIndex;
public readonly nint GameObjectAddress;
public readonly nint DrawObjectAddress;
public readonly bool LocalPlayerRelated;
public readonly bool PlayerRelated;
public readonly bool Networked;
public readonly string CollectionName;
public readonly string AnonymizedCollectionName;
public readonly List<ResourceNode> Nodes;
public readonly HashSet<ResourceNode> FlatNodes;
public readonly string Name = name;
public readonly string AnonymizedName = anonymizedName;
public readonly int GameObjectIndex = gameObjectIndex;
public readonly nint GameObjectAddress = gameObjectAddress;
public readonly nint DrawObjectAddress = drawObjectAddress;
public readonly bool LocalPlayerRelated = localPlayerRelated;
public readonly bool PlayerRelated = playerRelated;
public readonly bool Networked = networked;
public readonly string CollectionName = collectionName;
public readonly string AnonymizedCollectionName = anonymizedCollectionName;
public readonly List<ResourceNode> Nodes = [];
public readonly HashSet<ResourceNode> FlatNodes = [];
public int ModelId;
public CustomizeData CustomizeData;
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)
{
foreach (var node in Nodes)
@ -70,10 +66,22 @@ public class ResourceTree
};
ModelId = character->ModelContainer.ModelCharaId;
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);
// 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)
{
var slotContext = modelType switch
@ -90,18 +98,17 @@ public class ResourceTree
: globalContext.CreateContext(model, i),
};
var imc = (ResourceHandle*)model->IMCArray[i];
var imcNode = slotContext.CreateNodeFromImc(imc);
if (imcNode != null)
var imc = (ResourceHandle*)model->IMCArray[i];
if (slotContext.CreateNodeFromImc(imc) is { } imcNode)
{
if (globalContext.WithUiData)
imcNode.FallbackName = $"IMC #{i}";
Nodes.Add(imcNode);
}
var mdl = model->Models[i];
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
if (mdlNode != null)
var mdl = model->Models[i];
if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null,
i < mpapArray.Length ? mpapArray[(int)i].Value : null) is { } mdlNode)
{
if (globalContext.WithUiData)
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);
if (human != null)
if (human is not null)
AddHumanResources(globalContext, human);
}
@ -123,12 +132,12 @@ public class ResourceTree
var weaponNodes = new List<ResourceNode>();
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;
var subObject = (CharacterBase*)baseSubObject;
if (subObject->GetModelType() != ModelType.Weapon)
if (subObject->GetModelType() is not ModelType.Weapon)
continue;
var weapon = (Weapon*)subObject;
@ -140,22 +149,24 @@ public class ResourceTree
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)
{
var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType);
var imc = (ResourceHandle*)subObject->IMCArray[i];
var imcNode = slotContext.CreateNodeFromImc(imc);
if (imcNode != null)
var imc = (ResourceHandle*)subObject->IMCArray[i];
if (slotContext.CreateNodeFromImc(imc) is { } imcNode)
{
if (globalContext.WithUiData)
imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}";
weaponNodes.Add(imcNode);
}
var mdl = subObject->Models[i];
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
if (mdlNode != null)
var mdl = subObject->Models[i];
if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode)
{
if (globalContext.WithUiData)
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;
}
@ -176,28 +191,25 @@ public class ResourceTree
var genericContext = globalContext.CreateContext(&human->CharacterBase);
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)
&& genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle) is { } pbdNode)
{
var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle);
if (pbdNode != null)
if (globalContext.WithUiData)
{
if (globalContext.WithUiData)
{
pbdNode = pbdNode.Clone();
pbdNode.FallbackName = "Racial Deformer";
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
}
Nodes.Add(pbdNode);
pbdNode = pbdNode.Clone();
pbdNode.FallbackName = "Racial Deformer";
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
}
Nodes.Add(pbdNode);
}
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.Tex.TransparentPath;
var decalNode = genericContext.CreateNodeFromTex(human->Decal, decalPath);
if (decalNode != null)
if (genericContext.CreateNodeFromTex(human->Decal, decalPath) is { } decalNode)
{
if (globalContext.WithUiData)
{
@ -213,9 +225,9 @@ public class ResourceTree
var legacyDecalPath = hasLegacyDecal
? GamePaths.Human.Decal.LegacyDecalPath
: GamePaths.Tex.TransparentPath;
var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath);
if (legacyDecalNode != null)
if (genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath) is { } legacyDecalNode)
{
legacyDecalNode.ForceProtected = !hasLegacyDecal;
if (globalContext.WithUiData)
{
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);
if (eidNode != null)
@ -242,8 +255,9 @@ public class ResourceTree
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
{
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], (uint)i);
if (sklbNode != null)
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
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)
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);
}
}

View file

@ -33,6 +33,12 @@ internal static class StructExtensions
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)
{
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
@ -45,6 +51,12 @@ internal static class StructExtensions
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)
=> str == null ? CiByteString.Empty : new CiByteString(str).Clone();

View file

@ -1,8 +1,7 @@
using Dalamud.Interface;
using ImGuiNET;
using Lumina.Data;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.GameData.Files;
using Penumbra.Interop.ResourceTree;
@ -43,7 +42,7 @@ public partial class ModEditWindow
private void DrawQuickImportTab()
{
using var tab = ImRaii.TabItem("Import from Screen");
using var tab = ImUtf8.TabItem("Import from Screen"u8);
if (!tab)
{
_quickImportActions.Clear();
@ -73,14 +72,14 @@ public partial class ModEditWindow
else
{
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);
}
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.",
resourceNode.FullPath.FullName.Length == 0 || writable == null, true))
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize,
resourceNode.FullPath.FullName.Length is 0 || writable is null))
{
var fullPathStr = resourceNode.FullPath.FullName;
var ext = resourceNode.PossibleGamePaths.Length == 1
@ -110,15 +109,19 @@ public partial class ModEditWindow
_quickImportActions.Add((resourceNode.GamePath, writable), quickImport);
}
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize,
$"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true))
var canQuickImport = quickImport.CanExecute;
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();
_quickImportActions.Remove((resourceNode.GamePath, writable));
}
}
private record class RawFileWritable(string Path) : IWritable
private record RawFileWritable(string Path) : IWritable
{
public bool Valid
=> true;
@ -127,7 +130,7 @@ public partial class ModEditWindow
=> File.ReadAllBytes(Path);
}
private record class RawGameFileWritable(FileResource FileResource) : IWritable
private record RawGameFileWritable(FileResource FileResource) : IWritable
{
public bool Valid
=> true;
@ -185,19 +188,19 @@ public partial class ModEditWindow
public static QuickImportAction Prepare(ModEditWindow owner, Utf8GamePath gamePath, IWritable? file)
{
var editor = owner._editor;
if (editor == null)
if (editor is null)
return new QuickImportAction(owner._editor, FallbackOptionName, gamePath);
var subMod = editor.Option!;
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);
if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath))
return new QuickImportAction(editor, optionName, gamePath);
var mod = owner.Mod;
if (mod == null)
if (mod is null)
return new QuickImportAction(editor, optionName, gamePath);
var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport);
@ -232,7 +235,7 @@ public partial class ModEditWindow
{
var path = mod.ModPath;
var subDirs = 0;
if (subMod == null)
if (subMod is null)
return (path, subDirs);
var name = subMod.Name;