Merge branch 'rt-rework'

This commit is contained in:
Ottermandias 2023-11-17 13:09:04 +01:00
commit 806561b95a
34 changed files with 803 additions and 507 deletions

@ -1 +1 @@
Subproject commit f39a716ad4f908c301d497728ede047ee6bd61c0
Subproject commit ffdb966fec5a657893289e655c641ceb3af1d59f

View file

@ -1,5 +1,6 @@
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
@ -61,6 +62,26 @@ public struct EstCache : IDisposable
return manager.TemporarilySetFile(file, idx);
}
private readonly EstFile? GetEstFile(EstManipulation.EstType type)
{
return type switch
{
EstManipulation.EstType.Face => _estFaceFile,
EstManipulation.EstType.Hair => _estHairFile,
EstManipulation.EstType.Body => _estBodyFile,
EstManipulation.EstType.Head => _estHeadFile,
_ => null,
};
}
internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, SetId setId)
{
var file = GetEstFile(type);
return file != null
? file[genderRace, setId.Id]
: EstFile.GetDefault(manager, type, genderRace, setId);
}
public void Reset()
{
_estFaceFile?.Reset();

View file

@ -1,4 +1,5 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
@ -186,6 +187,18 @@ public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation,
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file)
=> _imcCache.GetImcFile(path, out file);
internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, SetId setId)
{
var eqdpFile = _eqdpCache.EqdpFile(race, accessory);
if (eqdpFile != null)
return setId.Id < eqdpFile.Count ? eqdpFile[setId] : default;
else
return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, setId);
}
internal ushort GetEstEntry(EstManipulation.EstType type, GenderRace genderRace, SetId setId)
=> _estCache.GetEstEntry(_manager, type, genderRace, setId);
/// <summary> Use this when CharacterUtility becomes ready. </summary>
private void ApplyStoredManipulations()
{

View file

@ -31,7 +31,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorTableTextures;
var colorSetTextures = DrawObject->ColorTableTextures;
if (colorSetTextures == null)
throw new InvalidOperationException("Draw object doesn't have color table textures");
@ -79,7 +79,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
textureSize[1] = TextureHeight;
using var texture =
new SafeTextureHandle(Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7), false);
new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false);
if (texture.IsInvalid)
return;
@ -88,7 +88,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
{
fixed (Half* colorTable = _colorTable)
{
success = Structs.TextureUtility.InitializeContents(texture.Texture, colorTable);
success = texture.Texture->InitializeContents(colorTable);
}
}
@ -101,7 +101,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
if (!base.IsStillValid())
return false;
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorTableTextures;
var colorSetTextures = DrawObject->ColorTableTextures;
if (colorSetTextures == null)
return false;

View file

@ -18,7 +18,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle;
var shpkHandle = mtrlHandle->ShaderPackageResourceHandle;
if (shpkHandle == null)
throw new InvalidOperationException("Material doesn't have a ShPk resource handle");
@ -61,7 +61,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
if (!CheckValidity())
return;
((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags;
Material->ShaderFlags = shPkFlags;
}
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)

View file

@ -93,7 +93,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy
continue;
var mtrlHandle = material->MaterialResourceHandle;
var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle);
var path = ResolveContext.GetResourceHandlePath(&mtrlHandle->ResourceHandle);
if (path == needle)
result.Add(new MaterialInfo(index, type, i, j));
}

View file

@ -102,8 +102,6 @@ public unsafe class MetaState : IDisposable
public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory)
{
var races = race.Dependencies();
if (races.Length == 0)
return DisposableContainer.Empty;
var equipmentEnumerable = equipment
? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))

View file

@ -31,10 +31,14 @@ public unsafe class PathState : IDisposable
private readonly ResolvePathHooks _monster;
private readonly ThreadLocal<ResolveData> _resolveData = new(() => ResolveData.Invalid, true);
private readonly ThreadLocal<uint> _internalResolve = new(() => 0, false);
public IList<ResolveData> CurrentData
=> _resolveData.Values;
public bool InInternalResolve
=> _internalResolve.Value != 0u;
public PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility, IGameInteropProvider interop)
{
interop.InitializeFromAttributes(this);
@ -55,6 +59,7 @@ public unsafe class PathState : IDisposable
public void Dispose()
{
_resolveData.Dispose();
_internalResolve.Dispose();
_human.Dispose();
_weapon.Dispose();
_demiHuman.Dispose();
@ -80,7 +85,10 @@ public unsafe class PathState : IDisposable
if (path == nint.Zero)
return path;
if (!InInternalResolve)
{
_resolveData.Value = collection.ToResolveData(gameObject);
}
return path;
}
@ -90,7 +98,37 @@ public unsafe class PathState : IDisposable
if (path == nint.Zero)
return path;
if (!InInternalResolve)
{
_resolveData.Value = data;
}
return path;
}
/// <summary>
/// Temporarily disables metadata mod application and resolve data capture on the current thread. <para />
/// Must be called to prevent race conditions between Penumbra's internal path resolution (for example for Resource Trees) and the game's path resolution. <para />
/// Please note that this will make path resolution cases that depend on metadata incorrect.
/// </summary>
/// <returns> A struct that will undo this operation when disposed. Best used with: <code>using (var _ = pathState.EnterInternalResolve()) { ... }</code> </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public InternalResolveRaii EnterInternalResolve()
=> new(this);
public readonly ref struct InternalResolveRaii
{
private readonly ThreadLocal<uint> _internalResolve;
public InternalResolveRaii(PathState parent)
{
_internalResolve = parent._internalResolve;
++_internalResolve.Value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public readonly void Dispose()
{
--_internalResolve.Value;
}
}
}

View file

@ -143,7 +143,7 @@ public unsafe class ResolvePathHooks : IDisposable
private nint ResolveMdlHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex)
{
var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
using var eqdp = slotIndex > 9
using var eqdp = slotIndex > 9 || _parent.InInternalResolve
? DisposableContainer.Empty
: _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), slotIndex < 5, slotIndex > 4);
return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex));
@ -176,6 +176,10 @@ public unsafe class ResolvePathHooks : IDisposable
private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data)
{
data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
if (_parent.InInternalResolve)
{
return DisposableContainer.Empty;
}
return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body),
data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair),

View file

@ -0,0 +1,312 @@
using Dalamud.Game.ClientState.Objects.Enums;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String;
using Penumbra.String.Classes;
using static Penumbra.Interop.Structs.CharacterBaseUtility;
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
namespace Penumbra.Interop.ResourceTree;
internal partial record ResolveContext
{
private Utf8GamePath ResolveModelPath()
{
// Correctness:
// Resolving a model path through the game's code can use EQDP metadata for human equipment models.
return ModelType switch
{
ModelType.Human when SlotIndex < 10 => ResolveEquipmentModelPath(),
_ => ResolveModelPathNative(),
};
}
private Utf8GamePath ResolveEquipmentModelPath()
{
var path = SlotIndex < 5
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot)
: GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot);
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private unsafe GenderRace ResolveModelRaceCode()
=> ResolveEqdpRaceCode(Slot, Equipment.Set);
private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, SetId setId)
{
var slotIndex = slot.ToIndex();
if (slotIndex >= 10 || ModelType != ModelType.Human)
return GenderRace.MidlanderMale;
var characterRaceCode = (GenderRace)((Human*)CharacterBase.Value)->RaceSexId;
if (characterRaceCode == GenderRace.MidlanderMale)
return GenderRace.MidlanderMale;
var accessory = slotIndex >= 5;
if ((ushort)characterRaceCode % 10 != 1 && accessory)
return GenderRace.MidlanderMale;
var metaCache = Global.Collection.MetaCache;
if (metaCache == null)
return GenderRace.MidlanderMale;
var entry = metaCache.GetEqdpEntry(characterRaceCode, accessory, setId);
if (entry.ToBits(slot).Item2)
return characterRaceCode;
var fallbackRaceCode = characterRaceCode.Fallback();
if (fallbackRaceCode == GenderRace.MidlanderMale)
return GenderRace.MidlanderMale;
entry = metaCache.GetEqdpEntry(fallbackRaceCode, accessory, setId);
if (entry.ToBits(slot).Item2)
return fallbackRaceCode;
return GenderRace.MidlanderMale;
}
private unsafe Utf8GamePath ResolveModelPathNative()
{
var path = ResolveMdlPath(CharacterBase, SlotIndex);
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private unsafe Utf8GamePath ResolveMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
{
// Safety and correctness:
// Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata.
return ModelType switch
{
ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName),
ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName),
ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName),
_ => ResolveMaterialPathNative(mtrlFileName),
};
}
private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
{
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
Span<byte> pathBuffer = stackalloc byte[260];
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
}
private unsafe Utf8GamePath ResolveWeaponMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
{
var setIdHigh = Equipment.Set.Id / 100;
// All MCH (20??) weapons' materials C are one and the same
if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c')
return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty;
// MNK (03??, 16??), NIN (18??) and DNC (26??) offhands share materials with the corresponding mainhand
if (setIdHigh is 3 or 16 or 18 or 26)
{
var setIdLow = Equipment.Set.Id % 100;
if (setIdLow > 50)
{
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
var mirroredSetId = (ushort)(Equipment.Set.Id - 50);
Span<byte> mirroredFileName = stackalloc byte[32];
mirroredFileName = mirroredFileName[..fileName.Length];
fileName.CopyTo(mirroredFileName);
WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId);
Span<byte> pathBuffer = stackalloc byte[260];
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName);
var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8);
if (weaponPosition >= 0)
WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId);
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
}
}
return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName);
}
private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
{
// TODO: Submit this (Monster->Variant) to ClientStructs
var variant = ResolveMaterialVariant(imc, ((byte*)CharacterBase.Value)[0x8F4]);
var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName);
Span<byte> pathBuffer = stackalloc byte[260];
pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName);
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
}
private unsafe byte ResolveMaterialVariant(ResourceHandle* imc, Variant variant)
{
var imcFileData = imc->GetDataSpan();
if (imcFileData.IsEmpty)
{
Penumbra.Log.Warning($"IMC resource handle with path {GetResourceHandlePath(imc, false)} doesn't have a valid data span");
return variant.Id;
}
var entry = ImcFile.GetEntry(imcFileData, Slot, variant, out var exists);
if (!exists)
return variant.Id;
return entry.MaterialId;
}
private static Span<byte> AssembleMaterialPath(Span<byte> materialPathBuffer, ReadOnlySpan<byte> modelPath, byte variant, ReadOnlySpan<byte> mtrlFileName)
{
var modelPosition = modelPath.IndexOf("/model/"u8);
if (modelPosition < 0)
return Span<byte>.Empty;
var baseDirectory = modelPath[..modelPosition];
baseDirectory.CopyTo(materialPathBuffer);
"/material/v"u8.CopyTo(materialPathBuffer[baseDirectory.Length..]);
WriteZeroPaddedNumber(materialPathBuffer.Slice(baseDirectory.Length + 11, 4), variant);
materialPathBuffer[baseDirectory.Length + 15] = (byte)'/';
mtrlFileName.CopyTo(materialPathBuffer[(baseDirectory.Length + 16)..]);
return materialPathBuffer[..(baseDirectory.Length + 16 + mtrlFileName.Length)];
}
private static void WriteZeroPaddedNumber(Span<byte> destination, ushort number)
{
for (var i = destination.Length; i-- > 0;)
{
destination[i] = (byte)('0' + number % 10);
number /= 10;
}
}
private unsafe Utf8GamePath ResolveMaterialPathNative(byte* mtrlFileName)
{
ByteString? path;
try
{
path = ResolveMtrlPath(CharacterBase, SlotIndex, mtrlFileName);
}
catch (AccessViolationException)
{
Penumbra.Log.Error($"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase.Value:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})");
return Utf8GamePath.Empty;
}
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private Utf8GamePath ResolveSkeletonPath(uint partialSkeletonIndex)
{
// Correctness and Safety:
// Resolving a skeleton 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 => ResolveHumanSkeletonPath(partialSkeletonIndex),
_ => ResolveSkeletonPathNative(partialSkeletonIndex),
};
}
private unsafe Utf8GamePath ResolveHumanSkeletonPath(uint partialSkeletonIndex)
{
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
if (set == 0)
return Utf8GamePath.Empty;
var path = GamePaths.Skeleton.Sklb.Path(raceCode, slot, set);
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanSkeletonData(uint partialSkeletonIndex)
{
var human = (Human*)CharacterBase.Value;
var characterRaceCode = (GenderRace)human->RaceSexId;
switch (partialSkeletonIndex)
{
case 0:
return (characterRaceCode, "base", 1);
case 1:
var faceId = human->FaceId;
var tribe = human->Customize[(int)CustomizeIndex.Tribe];
var modelType = human->Customize[(int)CustomizeIndex.ModelType];
if (faceId < 201)
{
faceId -= tribe switch
{
0xB when modelType == 4 => 100,
0xE | 0xF => 100,
_ => 0,
};
}
return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Face, faceId);
case 2:
return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Hair, human->HairId);
case 3:
return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstManipulation.EstType.Head);
case 4:
return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstManipulation.EstType.Body);
default:
return (0, string.Empty, 0);
}
}
private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstManipulation.EstType type)
{
var human = (Human*)CharacterBase.Value;
var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()];
return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set);
}
private unsafe (GenderRace RaceCode, string Slot, SetId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, SetId set)
{
var metaCache = Global.Collection.MetaCache;
var skeletonSet = metaCache == null ? default : metaCache.GetEstEntry(type, raceCode, set);
return (raceCode, EstManipulation.ToName(type), skeletonSet);
}
private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex)
{
var path = ResolveSklbPath(CharacterBase, partialSkeletonIndex);
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private Utf8GamePath ResolveSkeletonParameterPath(uint partialSkeletonIndex)
{
// Correctness and Safety:
// Resolving a skeleton parameter 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 => ResolveHumanSkeletonParameterPath(partialSkeletonIndex),
_ => ResolveSkeletonParameterPathNative(partialSkeletonIndex),
};
}
private Utf8GamePath ResolveHumanSkeletonParameterPath(uint partialSkeletonIndex)
{
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
if (set == 0)
return Utf8GamePath.Empty;
var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set);
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private unsafe Utf8GamePath ResolveSkeletonParameterPathNative(uint partialSkeletonIndex)
{
var path = ResolveSkpPath(CharacterBase, partialSkeletonIndex);
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
}

View file

@ -1,160 +1,189 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.Interop;
using OtterGui;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI;
using static Penumbra.Interop.Structs.CharacterBaseUtility;
using static Penumbra.Interop.Structs.ModelResourceHandleUtility;
using static Penumbra.Interop.Structs.StructExtensions;
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
namespace Penumbra.Interop.ResourceTree;
internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache,
int Skeleton, bool WithUiData)
internal record GlobalResolveContext(IObjectIdentifier Identifier, ModCollection Collection, TreeBuildCache TreeBuildCache, bool WithUiData)
{
public readonly Dictionary<nint, ResourceNode> Nodes = new(128);
public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128);
public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment)
=> new(Identifier, TreeBuildCache, Skeleton, WithUiData, Nodes, slot, equipment);
public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu,
EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, WeaponType weaponType = default)
=> new(this, characterBase, slotIndex, slot, equipment, weaponType);
}
internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, int Skeleton, bool WithUiData,
Dictionary<nint, ResourceNode> Nodes, EquipSlot Slot, CharacterArmor Equipment)
internal partial record ResolveContext(GlobalResolveContext Global, Pointer<CharacterBase> CharacterBase, uint SlotIndex,
EquipSlot Slot, CharacterArmor Equipment, WeaponType WeaponType)
{
private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true);
private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal)
{
if (Nodes.TryGetValue((nint)resourceHandle, out var cached))
return cached;
private unsafe ModelType ModelType
=> CharacterBase.Value->GetModelType();
private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath)
{
if (resourceHandle == null)
return null;
if (gamePath.IsEmpty)
return null;
if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false))
return null;
return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->Handle, path, @internal);
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path);
}
private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11)
private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11)
{
if (Nodes.TryGetValue((nint)resourceHandle, out var cached))
return cached;
if (resourceHandle == null)
return null;
Utf8GamePath path;
if (dx11)
{
var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/');
if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3)
return null;
if (gamePath[lastDirectorySeparator + 1] != (byte)'-' || gamePath[lastDirectorySeparator + 2] != (byte)'-')
{
Span<byte> prefixed = stackalloc byte[gamePath.Length + 2];
Span<byte> prefixed = stackalloc byte[260];
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
prefixed[lastDirectorySeparator + 1] = (byte)'-';
prefixed[lastDirectorySeparator + 2] = (byte)'-';
gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]);
if (!Utf8GamePath.FromSpan(prefixed, out var tmp))
if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], out var tmp))
return null;
gamePath = tmp.Path.Clone();
path = tmp.Clone();
}
}
if (!Utf8GamePath.FromByteString(gamePath, out var path))
return null;
return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal);
}
private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
Utf8GamePath gamePath, bool @internal)
else
{
// Make sure the game path is owned, otherwise stale trees could cause crashes (access violations) or other memory safety issues.
if (!gamePath.IsOwned)
gamePath = gamePath.Clone();
if (!Utf8GamePath.FromByteString(gamePath, out path))
return null;
}
return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path);
}
private unsafe ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
Utf8GamePath gamePath)
{
if (resourceHandle == null)
throw new ArgumentNullException(nameof(resourceHandle));
if (Global.Nodes.TryGetValue((gamePath, (nint)resourceHandle), out var cached))
return cached;
return CreateNode(type, objectAddress, resourceHandle, gamePath);
}
private unsafe ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
Utf8GamePath gamePath, bool autoAdd = true)
{
if (resourceHandle == null)
throw new ArgumentNullException(nameof(resourceHandle));
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty;
var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), @internal, this)
var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), this)
{
GamePath = gamePath,
FullPath = fullPath,
};
if (resourceHandle != null)
Nodes.Add((nint)resourceHandle, node);
if (autoAdd)
Global.Nodes.Add((gamePath, (nint)resourceHandle), node);
return node;
}
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal)
public unsafe ResourceNode? CreateNodeFromEid(ResourceHandle* eid)
{
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty;
if (fullPath.InternalName.IsEmpty)
if (eid == null)
return null;
return new ResourceNode(type, objectAddress, (nint)handle, GetResourceHandleLength(handle), @internal, this)
{
FullPath = fullPath,
};
if (!Utf8GamePath.FromByteString(ResolveEidPath(CharacterBase), out var path))
return null;
return GetOrCreateNode(ResourceType.Eid, 0, eid, path);
}
public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc)
{
if (Nodes.TryGetValue((nint)imc, out var cached))
return cached;
var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true);
if (node == null)
if (imc == null)
return null;
Nodes.Add((nint)imc, node);
if (!Utf8GamePath.FromByteString(ResolveImcPath(CharacterBase, SlotIndex), out var path))
return null;
return node;
return GetOrCreateNode(ResourceType.Imc, 0, imc, path);
}
public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex)
public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath)
{
if (Nodes.TryGetValue((nint)tex, out var cached))
return cached;
if (tex == null)
return null;
var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false);
if (node != null)
Nodes.Add((nint)tex, node);
if (!Utf8GamePath.FromString(gamePath, out var path))
return null;
return node;
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path);
}
public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl)
public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc)
{
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
if (mdl == null || mdl->ModelResourceHandle == null)
return null;
var mdlResource = mdl->ModelResourceHandle;
if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path))
return null;
if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached))
if (Global.Nodes.TryGetValue((path, (nint)mdlResource), out var cached))
return cached;
var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, mdl->ResourceHandle, false);
if (node == null)
return null;
var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdlResource->ResourceHandle, path, false);
for (var i = 0; i < mdl->MaterialCount; i++)
{
var mtrl = (Material*)mdl->Materials[i];
var mtrlNode = CreateNodeFromMaterial(mtrl);
var mtrl = mdl->Materials[i];
if (mtrl == null)
continue;
var mtrlFileName = GetMaterialFileNameBySlot(mdlResource, (uint)i);
var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imc, mtrlFileName));
if (mtrlNode != null)
{
if (WithUiData)
if (Global.WithUiData)
mtrlNode.FallbackName = $"Material #{i}";
node.Children.Add(mtrlNode);
}
}
Nodes.Add((nint)mdl->ResourceHandle, node);
Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node);
return node;
}
private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl)
private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path)
{
static ushort GetTextureIndex(Material* mtrl, ushort texFlags, HashSet<uint> alreadyVisitedSamplerIds)
{
@ -169,55 +198,55 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
}
static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet<uint> alreadyVisitedSamplerIds)
=> mtrl->TextureSpan.FindFirst(p => p.ResourceHandle == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p)
=> mtrl->TexturesSpan.FindFirst(p => p.Texture == handle && !alreadyVisitedSamplerIds.Contains(p.Id), out var p)
? p.Id
: null;
static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id)
=> new ReadOnlySpan<ShaderPackageUtility.Sampler>(shpk->Samplers, shpk->SamplerCount).FindFirst(s => s.Id == id, out var s)
? s.Crc
=> shpk->SamplersSpan.FindFirst(s => s.Id == id, out var s)
? s.CRC
: null;
if (mtrl == null)
if (mtrl == null || mtrl->MaterialResourceHandle == null)
return null;
var resource = mtrl->ResourceHandle;
if (Nodes.TryGetValue((nint)resource, out var cached))
var resource = mtrl->MaterialResourceHandle;
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
return cached;
var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->Handle, false);
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false);
if (node == null)
return null;
var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false);
var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName));
if (shpkNode != null)
{
if (WithUiData)
if (Global.WithUiData)
shpkNode.Name = "Shader Package";
node.Children.Add(shpkNode);
}
var shpkFile = WithUiData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null;
var shpk = WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
var shpkFile = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null;
var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
var alreadyProcessedSamplerIds = new HashSet<uint>();
for (var i = 0; i < resource->NumTex; i++)
for (var i = 0; i < resource->TextureCount; i++)
{
var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false,
resource->TexIsDX11(i));
var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new ByteString(resource->TexturePath(i)),
resource->Textures[i].IsDX11);
if (texNode == null)
continue;
if (WithUiData)
if (Global.WithUiData)
{
string? name = null;
if (shpk != null)
{
var index = GetTextureIndex(mtrl, resource->TexSpace[i].Flags, alreadyProcessedSamplerIds);
var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds);
uint? samplerId;
if (index != 0x001F)
samplerId = mtrl->Textures[index].Id;
else
samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle, alreadyProcessedSamplerIds);
samplerId = GetTextureSamplerId(mtrl, resource->Textures[i].TextureResourceHandle, alreadyProcessedSamplerIds);
if (samplerId.HasValue)
{
alreadyProcessedSamplerIds.Add(samplerId.Value);
@ -234,94 +263,61 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
node.Children.Add(texNode);
}
Nodes.Add((nint)resource, node);
Global.Nodes.Add((path, (nint)resource), node);
return node;
}
public unsafe ResourceNode? CreateNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb)
public unsafe ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
{
if (sklb->SkeletonResourceHandle == null)
if (sklb == null || sklb->SkeletonResourceHandle == null)
return null;
if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached))
var path = ResolveSkeletonPath(partialSkeletonIndex);
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached))
return cached;
var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false);
var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false);
if (node != null)
{
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb);
var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex);
if (skpNode != null)
node.Children.Add(skpNode);
Nodes.Add((nint)sklb->SkeletonResourceHandle, node);
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
}
return node;
}
private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb)
private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex)
{
if (sklb->SkeletonParameterResourceHandle == null)
if (sklb == null || sklb->SkeletonParameterResourceHandle == null)
return null;
if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached))
var path = ResolveSkeletonParameterPath(partialSkeletonIndex);
if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached))
return cached;
var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true);
var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, path, false);
if (node != null)
{
if (WithUiData)
if (Global.WithUiData)
node.FallbackName = "Skeleton Parameters";
Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node);
Global.Nodes.Add((path, (nint)sklb->SkeletonParameterResourceHandle), node);
}
return node;
}
internal List<Utf8GamePath> FilterGamePaths(IReadOnlyCollection<Utf8GamePath> gamePaths)
{
var filtered = new List<Utf8GamePath>(gamePaths.Count);
foreach (var path in gamePaths)
{
// In doubt, keep the paths.
if (IsMatch(path.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries))
?? true)
filtered.Add(path);
}
return filtered;
}
private bool? IsMatch(ReadOnlySpan<string> path)
=> SafeGet(path, 0) switch
{
"chara" => SafeGet(path, 1) switch
{
"accessory" => IsMatchEquipment(path[2..], $"a{Equipment.Set.Id:D4}"),
"equipment" => IsMatchEquipment(path[2..], $"e{Equipment.Set.Id:D4}"),
"monster" => SafeGet(path, 2) == $"m{Skeleton:D4}",
"weapon" => IsMatchEquipment(path[2..], $"w{Equipment.Set.Id:D4}"),
_ => null,
},
_ => null,
};
private bool? IsMatchEquipment(ReadOnlySpan<string> path, string equipmentDir)
=> SafeGet(path, 0) == equipmentDir
? SafeGet(path, 1) switch
{
"material" => SafeGet(path, 2) == $"v{Equipment.Variant.Id:D4}",
_ => null,
}
: false;
internal ResourceNode.UiData GuessModelUIData(Utf8GamePath gamePath)
{
var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries);
// Weapons intentionally left out.
var isEquipment = SafeGet(path, 0) == "chara" && SafeGet(path, 1) is "accessory" or "equipment";
if (isEquipment)
foreach (var item in Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot()))
foreach (var item in Global.Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot()))
{
var name = Slot switch
{
@ -344,7 +340,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
internal ResourceNode.UiData GuessUIDataFromPath(Utf8GamePath gamePath)
{
foreach (var obj in Identifier.Identify(gamePath.ToString()))
foreach (var obj in Global.Identifier.Identify(gamePath.ToString()))
{
var name = obj.Key;
if (name.StartsWith("Customization:"))
@ -362,16 +358,16 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
return i >= 0 && i < array.Length ? array[i] : null;
}
internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle)
internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle, bool stripPrefix = true)
{
if (handle == null)
return ByteString.Empty;
var name = handle->FileName();
var name = handle->FileName.AsByteString();
if (name.IsEmpty)
return ByteString.Empty;
if (name[0] == (byte)'|')
if (stripPrefix && name[0] == (byte)'|')
{
var pos = name.IndexOf((byte)'|', 1);
if (pos < 0)
@ -388,6 +384,6 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree
if (handle == null)
return 0;
return ResourceHandle.GetLength(handle);
return handle->GetLength();
}
}

View file

@ -15,7 +15,6 @@ public class ResourceNode : ICloneable
public Utf8GamePath[] PossibleGamePaths;
public FullPath FullPath;
public readonly ulong Length;
public readonly bool Internal;
public readonly List<ResourceNode> Children;
internal ResolveContext? ResolveContext;
@ -31,14 +30,16 @@ public class ResourceNode : ICloneable
}
}
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, bool @internal, ResolveContext? resolveContext)
public bool Internal
=> Type is ResourceType.Eid or ResourceType.Imc;
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
{
Type = type;
ObjectAddress = objectAddress;
ResourceHandle = resourceHandle;
PossibleGamePaths = Array.Empty<Utf8GamePath>();
Length = length;
Internal = @internal;
Children = new List<ResourceNode>();
ResolveContext = resolveContext;
}
@ -54,7 +55,6 @@ public class ResourceNode : ICloneable
PossibleGamePaths = other.PossibleGamePaths;
FullPath = other.FullPath;
Length = other.Length;
Internal = other.Internal;
Children = other.Children;
ResolveContext = other.ResolveContext;
}

View file

@ -1,9 +1,12 @@
using Dalamud.Game.ClientState.Objects.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.String.Classes;
using Penumbra.UI;
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
@ -50,21 +53,28 @@ public class ResourceTree
{
var character = (Character*)GameObjectAddress;
var model = (CharacterBase*)DrawObjectAddress;
var equipment = new ReadOnlySpan<CharacterArmor>(&character->DrawData.Head, 10);
// var customize = new ReadOnlySpan<byte>( character->CustomizeData, 26 );
var modelType = model->GetModelType();
var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null;
var equipment = modelType switch
{
CharacterBase.ModelType.Human => new ReadOnlySpan<CharacterArmor>(&human->Head, 10),
CharacterBase.ModelType.DemiHuman => new ReadOnlySpan<CharacterArmor>(&character->DrawData.Head, 10),
_ => ReadOnlySpan<CharacterArmor>.Empty,
};
ModelId = character->CharacterData.ModelCharaId;
CustomizeData = character->DrawData.CustomizeData;
RaceCode = model->GetModelType() == CharacterBase.ModelType.Human ? (GenderRace)((Human*)model)->RaceSexId : GenderRace.Unknown;
RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;
var genericContext = globalContext.CreateContext(model);
for (var i = 0; i < model->SlotCount; ++i)
{
var context = globalContext.CreateContext(
i < equipment.Length ? ((uint)i).ToEquipSlot() : EquipSlot.Unknown,
i < equipment.Length ? equipment[i] : default
);
var slotContext = i < equipment.Length
? globalContext.CreateContext(model, (uint)i, ((uint)i).ToEquipSlot(), equipment[i])
: globalContext.CreateContext(model, (uint)i);
var imc = (ResourceHandle*)model->IMCArray[i];
var imcNode = context.CreateNodeFromImc(imc);
var imcNode = slotContext.CreateNodeFromImc(imc);
if (imcNode != null)
{
if (globalContext.WithUiData)
@ -72,8 +82,8 @@ public class ResourceTree
Nodes.Add(imcNode);
}
var mdl = (RenderModel*)model->Models[i];
var mdlNode = context.CreateNodeFromRenderModel(mdl);
var mdl = model->Models[i];
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
if (mdlNode != null)
{
if (globalContext.WithUiData)
@ -82,62 +92,74 @@ public class ResourceTree
}
}
AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton);
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton);
if (model->GetModelType() == CharacterBase.ModelType.Human)
AddHumanResources(globalContext, (HumanExt*)model);
AddWeapons(globalContext, model);
if (human != null)
AddHumanResources(globalContext, human);
}
private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human)
private unsafe void AddWeapons(GlobalResolveContext globalContext, CharacterBase* model)
{
var firstSubObject = (CharacterBase*)human->Human.CharacterBase.DrawObject.Object.ChildObject;
if (firstSubObject != null)
var weaponIndex = 0;
var weaponNodes = new List<ResourceNode>();
foreach (var baseSubObject in model->DrawObject.Object.ChildObjects)
{
var subObjectNodes = new List<ResourceNode>();
var subObject = firstSubObject;
var subObjectIndex = 0;
do
{
var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null;
var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc.";
var subObjectContext = globalContext.CreateContext(
weapon != null ? EquipSlot.MainHand : EquipSlot.Unknown,
weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default
);
if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase)
continue;
var subObject = (CharacterBase*)baseSubObject;
if (subObject->GetModelType() != CharacterBase.ModelType.Weapon)
continue;
var weapon = (Weapon*)subObject;
// This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it.
var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand;
var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown);
var weaponType = weapon->SecondaryId;
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
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 = subObjectContext.CreateNodeFromImc(imc);
var imcNode = slotContext.CreateNodeFromImc(imc);
if (imcNode != null)
{
if (globalContext.WithUiData)
imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}";
subObjectNodes.Add(imcNode);
imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}";
weaponNodes.Add(imcNode);
}
var mdl = (RenderModel*)subObject->Models[i];
var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl);
var mdl = subObject->Models[i];
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
if (mdlNode != null)
{
if (globalContext.WithUiData)
mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}";
subObjectNodes.Add(mdlNode);
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
weaponNodes.Add(mdlNode);
}
}
AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, ");
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, ");
subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject;
++subObjectIndex;
} while (subObject != null && subObject != firstSubObject);
Nodes.InsertRange(0, subObjectNodes);
++weaponIndex;
}
Nodes.InsertRange(0, weaponNodes);
}
var context = globalContext.CreateContext(EquipSlot.Unknown, default);
private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human)
{
var genericContext = globalContext.CreateContext(&human->CharacterBase);
var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal);
var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F);
var decalPath = decalId != 0
? GamePaths.Human.Decal.FaceDecalPath(decalId)
: GamePaths.Tex.TransparentPath;
var decalNode = genericContext.CreateNodeFromTex(human->Decal, decalPath);
if (decalNode != null)
{
if (globalContext.WithUiData)
@ -149,7 +171,11 @@ public class ResourceTree
Nodes.Add(decalNode);
}
var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal);
var hasLegacyDecal = (human->Customize[(int)CustomizeIndex.FaceFeatures] & 0x80) != 0;
var legacyDecalPath = hasLegacyDecal
? GamePaths.Human.Decal.LegacyDecalPath
: GamePaths.Tex.TransparentPath;
var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath);
if (legacyDecalNode != null)
{
if (globalContext.WithUiData)
@ -162,17 +188,25 @@ public class ResourceTree
}
}
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, Skeleton* skeleton, string prefix = "")
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, string prefix = "")
{
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
if (eidNode != null)
{
if (context.Global.WithUiData)
eidNode.FallbackName = $"{prefix}EID";
Nodes.Add(eidNode);
}
if (skeleton == null)
return;
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
{
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]);
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], (uint)i);
if (sklbNode != null)
{
if (context.WithUiData)
if (context.Global.WithUiData)
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
nodes.Add(sklbNode);
}

View file

@ -1,5 +1,4 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Api.Enums;
using Penumbra.Collections;
@ -18,9 +17,10 @@ public class ResourceTreeFactory
private readonly IdentifierService _identifier;
private readonly Configuration _config;
private readonly ActorService _actors;
private readonly PathState _pathState;
public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, CollectionResolver resolver, IdentifierService identifier,
Configuration config, ActorService actors)
Configuration config, ActorService actors, PathState pathState)
{
_gameData = gameData;
_objects = objects;
@ -28,6 +28,7 @@ public class ResourceTreeFactory
_identifier = identifier;
_config = config;
_actors = actors;
_pathState = pathState;
}
private TreeBuildCache CreateTreeBuildCache()
@ -87,13 +88,17 @@ public class ResourceTreeFactory
var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId;
var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related,
networked, collectionResolveData.ModCollection.Name);
var globalContext = new GlobalResolveContext(_identifier.AwaitedService, cache,
((Character*)gameObjStruct)->CharacterData.ModelCharaId, (flags & Flags.WithUiData) != 0);
var globalContext = new GlobalResolveContext(_identifier.AwaitedService, collectionResolveData.ModCollection,
cache, (flags & Flags.WithUiData) != 0);
using (var _ = _pathState.EnterInternalResolve())
{
tree.LoadResources(globalContext);
}
tree.FlatNodes.UnionWith(globalContext.Nodes.Values);
tree.ProcessPostfix((node, _) => tree.FlatNodes.Add(node));
ResolveGamePaths(tree, collectionResolveData.ModCollection);
// This is currently unneeded as we can resolve all paths by querying the draw object:
// ResolveGamePaths(tree, collectionResolveData.ModCollection);
if (globalContext.WithUiData)
ResolveUiData(tree);
FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? _config.ModDirectory : null);
@ -128,23 +133,15 @@ public class ResourceTreeFactory
if (!reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet))
continue;
IReadOnlyCollection<Utf8GamePath> resolvedList = resolvedSet;
if (resolvedList.Count > 1)
{
var filteredList = node.ResolveContext!.FilterGamePaths(resolvedList);
if (filteredList.Count > 0)
resolvedList = filteredList;
}
if (resolvedList.Count != 1)
if (resolvedSet.Count != 1)
{
Penumbra.Log.Debug(
$"Found {resolvedList.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:");
foreach (var gamePath in resolvedList)
$"Found {resolvedSet.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:");
foreach (var gamePath in resolvedSet)
Penumbra.Log.Debug($"Game path: {gamePath}");
}
node.PossibleGamePaths = resolvedList.ToArray();
node.PossibleGamePaths = resolvedSet.ToArray();
}
else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1)
{

View file

@ -1,5 +1,4 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Penumbra.Interop.Structs;
namespace Penumbra.Interop.SafeHandles;
@ -18,7 +17,7 @@ public unsafe class SafeTextureHandle : SafeHandle
throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported");
if (incRef && handle != null)
TextureUtility.IncRef(handle);
handle->IncRef();
SetHandle((nint)handle);
}
@ -43,7 +42,7 @@ public unsafe class SafeTextureHandle : SafeHandle
}
if (handle != 0)
TextureUtility.DecRef((Texture*)handle);
((Texture*)handle)->DecRef();
return true;
}

View file

@ -2,6 +2,7 @@ using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using OtterGui.Classes;
using Penumbra.Communication;
using Penumbra.GameData;
@ -73,19 +74,19 @@ public sealed unsafe class SkinFixer : IDisposable
public ulong GetAndResetSlowPathCallDelta()
=> Interlocked.Exchange(ref _slowPathCallDelta, 0);
private static bool IsSkinMaterial(Structs.MtrlResource* mtrlResource)
private static bool IsSkinMaterial(MaterialResourceHandle* mtrlResource)
{
if (mtrlResource == null)
return false;
var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkString);
var shpkName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlResource->ShpkName);
return SkinShpkName.SequenceEqual(shpkName);
}
private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject)
{
var mtrl = (Structs.MtrlResource*)mtrlResourceHandle;
var shpk = mtrl->ShpkResourceHandle;
var mtrl = (MaterialResourceHandle*)mtrlResourceHandle;
var shpk = mtrl->ShaderPackageResourceHandle;
if (shpk == null)
return;
@ -109,7 +110,7 @@ public sealed unsafe class SkinFixer : IDisposable
return _onRenderMaterialHook.Original(human, param);
var material = param->Model->Materials[param->MaterialIndex];
var mtrlResource = (Structs.MtrlResource*)material->MaterialResourceHandle;
var mtrlResource = material->MaterialResourceHandle;
if (!IsSkinMaterial(mtrlResource))
return _onRenderMaterialHook.Original(human, param);
@ -124,7 +125,7 @@ public sealed unsafe class SkinFixer : IDisposable
{
try
{
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShpkResourceHandle;
_utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle;
return _onRenderMaterialHook.Original(human, param);
}
finally

View file

@ -1,14 +0,0 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
namespace Penumbra.Interop.Structs;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct CharacterBaseExt
{
[FieldOffset(0x0)]
public CharacterBase CharacterBase;
[FieldOffset(0x258)]
public Texture** ColorTableTextures;
}

View file

@ -0,0 +1,62 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.String;
namespace Penumbra.Interop.Structs;
// TODO submit these to ClientStructs
public static unsafe class CharacterBaseUtility
{
private const int PathBufferSize = 260;
private const uint ResolveSklbPathVf = 72;
private const uint ResolveMdlPathVf = 73;
private const uint ResolveSkpPathVf = 74;
private const uint ResolveImcPathVf = 81;
private const uint ResolveMtrlPathVf = 82;
private const uint ResolveEidPathVf = 85;
private static void* GetVFunc(CharacterBase* characterBase, uint vfIndex)
=> ((void**)characterBase->VTable)[vfIndex];
private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex)
{
var vFunc = (delegate* unmanaged<CharacterBase*, byte*, nint, byte*>)GetVFunc(characterBase, vfIndex);
var pathBuffer = stackalloc byte[PathBufferSize];
var path = vFunc(characterBase, pathBuffer, PathBufferSize);
return path != null ? new ByteString(path).Clone() : null;
}
private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex, uint slotIndex)
{
var vFunc = (delegate* unmanaged<CharacterBase*, byte*, nint, uint, byte*>)GetVFunc(characterBase, vfIndex);
var pathBuffer = stackalloc byte[PathBufferSize];
var path = vFunc(characterBase, pathBuffer, PathBufferSize, slotIndex);
return path != null ? new ByteString(path).Clone() : null;
}
private static ByteString? ResolvePath(CharacterBase* characterBase, uint vfIndex, uint slotIndex, byte* name)
{
var vFunc = (delegate* unmanaged<CharacterBase*, byte*, nint, uint, byte*, byte*>)GetVFunc(characterBase, vfIndex);
var pathBuffer = stackalloc byte[PathBufferSize];
var path = vFunc(characterBase, pathBuffer, PathBufferSize, slotIndex, name);
return path != null ? new ByteString(path).Clone() : null;
}
public static ByteString? ResolveEidPath(CharacterBase* characterBase)
=> ResolvePath(characterBase, ResolveEidPathVf);
public static ByteString? ResolveImcPath(CharacterBase* characterBase, uint slotIndex)
=> ResolvePath(characterBase, ResolveImcPathVf, slotIndex);
public static ByteString? ResolveMdlPath(CharacterBase* characterBase, uint slotIndex)
=> ResolvePath(characterBase, ResolveMdlPathVf, slotIndex);
public static ByteString? ResolveMtrlPath(CharacterBase* characterBase, uint slotIndex, byte* mtrlFileName)
=> ResolvePath(characterBase, ResolveMtrlPathVf, slotIndex, mtrlFileName);
public static ByteString? ResolveSklbPath(CharacterBase* characterBase, uint partialSkeletonIndex)
=> ResolvePath(characterBase, ResolveSklbPathVf, partialSkeletonIndex);
public static ByteString? ResolveSkpPath(CharacterBase* characterBase, uint partialSkeletonIndex)
=> ResolvePath(characterBase, ResolveSkpPathVf, partialSkeletonIndex);
}

View file

@ -1,28 +0,0 @@
namespace Penumbra.Interop.Structs;
[StructLayout(LayoutKind.Explicit, Size = 0x70)]
public unsafe struct ConstantBuffer
{
[FieldOffset(0x20)]
public int Size;
[FieldOffset(0x24)]
public int Flags;
[FieldOffset(0x28)]
private void* _maybeSourcePointer;
public bool TryGetBuffer(out Span<float> buffer)
{
if ((Flags & 0x4003) == 0 && _maybeSourcePointer != null)
{
buffer = new Span<float>(_maybeSourcePointer, Size >> 2);
return true;
}
else
{
buffer = null;
return false;
}
}
}

View file

@ -1,19 +0,0 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
namespace Penumbra.Interop.Structs;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct HumanExt
{
[FieldOffset(0x0)]
public Human Human;
[FieldOffset(0x0)]
public CharacterBaseExt CharacterBase;
[FieldOffset(0x9E8)]
public ResourceHandle* Decal;
[FieldOffset(0x9F0)]
public ResourceHandle* LegacyBodyDecal;
}

View file

@ -1,47 +0,0 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
namespace Penumbra.Interop.Structs;
[StructLayout(LayoutKind.Explicit, Size = 0x40)]
public unsafe struct Material
{
[FieldOffset(0x10)]
public MtrlResource* ResourceHandle;
[FieldOffset(0x18)]
public uint ShaderPackageFlags;
[FieldOffset(0x20)]
public uint* ShaderKeys;
public int ShaderKeyCount
=> (int)((uint*)Textures - ShaderKeys);
[FieldOffset(0x28)]
public ConstantBuffer* MaterialParameter;
[FieldOffset(0x30)]
public TextureEntry* Textures;
[FieldOffset(0x38)]
public ushort TextureCount;
public Texture* Texture(int index)
=> Textures[index].ResourceHandle->KernelTexture;
[StructLayout(LayoutKind.Explicit, Size = 0x18)]
public struct TextureEntry
{
[FieldOffset(0x00)]
public uint Id;
[FieldOffset(0x08)]
public TextureResourceHandle* ResourceHandle;
[FieldOffset(0x10)]
public uint SamplerFlags;
}
public ReadOnlySpan<TextureEntry> TextureSpan
=> new(Textures, TextureCount);
}

View file

@ -0,0 +1,18 @@
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
namespace Penumbra.Interop.Structs;
// TODO submit this to ClientStructs
public class ModelResourceHandleUtility
{
public ModelResourceHandleUtility(IGameInteropProvider interop)
=> interop.InitializeFromAttributes(this);
[Signature("E8 ?? ?? ?? ?? 44 8B CD 48 89 44 24")]
private static nint _getMaterialFileNameBySlot = nint.Zero;
public static unsafe byte* GetMaterialFileNameBySlot(ModelResourceHandle* handle, uint slot)
=> ((delegate* unmanaged<ModelResourceHandle*, uint, byte*>)_getMaterialFileNameBySlot)(handle, slot);
}

View file

@ -1,45 +0,0 @@
namespace Penumbra.Interop.Structs;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct MtrlResource
{
[FieldOffset(0x00)]
public ResourceHandle Handle;
[FieldOffset(0xC8)]
public ShaderPackageResourceHandle* ShpkResourceHandle;
[FieldOffset(0xD0)]
public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list.
[FieldOffset(0xE0)]
public byte* StringList;
[FieldOffset(0xF8)]
public ushort ShpkOffset;
[FieldOffset(0xFA)]
public byte NumTex;
public byte* ShpkString
=> StringList + ShpkOffset;
public byte* TexString(int idx)
=> StringList + TexSpace[idx].PathOffset;
public bool TexIsDX11(int idx)
=> TexSpace[idx].Flags >= 0x8000;
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct TextureEntry
{
[FieldOffset(0x00)]
public TextureResourceHandle* ResourceHandle;
[FieldOffset(0x08)]
public ushort PathOffset;
[FieldOffset(0x0A)]
public ushort Flags;
}
}

View file

@ -5,6 +5,9 @@ namespace Penumbra.Interop.Structs;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct RenderModel
{
[FieldOffset(0)]
public Model Model;
[FieldOffset(0x18)]
public RenderModel* PreviousModel;

View file

@ -1,10 +1,8 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums;
using Penumbra.GameData;
using Penumbra.String;
using Penumbra.String.Classes;
using CsHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
namespace Penumbra.Interop.Structs;
@ -14,24 +12,8 @@ public unsafe struct TextureResourceHandle
[FieldOffset(0x0)]
public ResourceHandle Handle;
[FieldOffset(0x38)]
public IntPtr Unk;
[FieldOffset(0x118)]
public Texture* KernelTexture;
[FieldOffset(0x20)]
public IntPtr NewKernelTexture;
}
[StructLayout(LayoutKind.Explicit)]
public unsafe struct ShaderPackageResourceHandle
{
[FieldOffset(0x0)]
public ResourceHandle Handle;
[FieldOffset(0xB0)]
public ShaderPackage* ShaderPackage;
public CsHandle.TextureResourceHandle CsHandle;
}
public enum LoadState : byte
@ -59,27 +41,14 @@ public unsafe struct ResourceHandle
public ulong DataLength;
}
public const int SsoSize = 15;
public readonly ByteString FileName()
=> CsHandle.FileName.AsByteString();
public byte* FileNamePtr()
{
if (FileNameLength > SsoSize)
return FileNameData;
public readonly bool GamePath(out Utf8GamePath path)
=> Utf8GamePath.FromSpan(CsHandle.FileName.AsSpan(), out path);
fixed (byte** name = &FileNameData)
{
return (byte*)name;
}
}
public ByteString FileName()
=> ByteString.FromByteStringUnsafe(FileNamePtr(), FileNameLength, true);
public ReadOnlySpan<byte> FileNameAsSpan()
=> new(FileNamePtr(), FileNameLength);
public bool GamePath(out Utf8GamePath path)
=> Utf8GamePath.FromSpan(FileNameAsSpan(), out path);
[FieldOffset(0x00)]
public CsHandle.ResourceHandle CsHandle;
[FieldOffset(0x00)]
public void** VTable;
@ -90,18 +59,9 @@ public unsafe struct ResourceHandle
[FieldOffset(0x0C)]
public ResourceType FileType;
[FieldOffset(0x10)]
public uint Id;
[FieldOffset(0x28)]
public uint FileSize;
[FieldOffset(0x2C)]
public uint FileSize2;
[FieldOffset(0x34)]
public uint FileSize3;
[FieldOffset(0x48)]
public byte* FileNameData;
@ -114,13 +74,6 @@ public unsafe struct ResourceHandle
[FieldOffset(0xAC)]
public uint RefCount;
// May return null.
public static byte* GetData(ResourceHandle* handle)
=> ((delegate* unmanaged< ResourceHandle*, byte* >)handle->VTable[Offsets.ResourceHandleGetDataVfunc])(handle);
public static ulong GetLength(ResourceHandle* handle)
=> ((delegate* unmanaged< ResourceHandle*, ulong >)handle->VTable[Offsets.ResourceHandleGetLengthVfunc])(handle);
// Only use these if you know what you are doing.
// Those are actually only sure to be accessible for DefaultResourceHandles.

View file

@ -1,17 +0,0 @@
namespace Penumbra.Interop.Structs;
public static class ShaderPackageUtility
{
[StructLayout(LayoutKind.Explicit, Size = 0xC)]
public unsafe struct Sampler
{
[FieldOffset(0x0)]
public uint Crc;
[FieldOffset(0x4)]
public uint Id;
[FieldOffset(0xA)]
public ushort Slot;
}
}

View file

@ -0,0 +1,24 @@
using FFXIVClientStructs.STD;
using Penumbra.String;
namespace Penumbra.Interop.Structs;
internal static class StructExtensions
{
// TODO submit this to ClientStructs
public static unsafe ReadOnlySpan<byte> AsSpan(in this StdString str)
{
if (str.Length < 16)
{
fixed (StdString* pStr = &str)
{
return new(pStr->Buffer, (int)str.Length);
}
}
else
return new(str.BufferPtr, (int)str.Length);
}
public static unsafe ByteString AsByteString(in this StdString str)
=> ByteString.FromSpanUnsafe(str.AsSpan(), true);
}

View file

@ -1,31 +0,0 @@
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
namespace Penumbra.Interop.Structs;
public unsafe class TextureUtility
{
public TextureUtility(IGameInteropProvider interop)
=> interop.InitializeFromAttributes(this);
[Signature("E8 ?? ?? ?? ?? 8B 0F 48 8D 54 24")]
private static nint _textureCreate2D = nint.Zero;
[Signature("E9 ?? ?? ?? ?? 8B 02 25")]
private static nint _textureInitializeContents = nint.Zero;
public static Texture* Create2D(Device* device, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk)
=> ((delegate* unmanaged<Device*, int*, byte, uint, uint, uint, Texture*>)_textureCreate2D)(device, size, mipLevel, textureFormat,
flags, unk);
public static bool InitializeContents(Texture* texture, void* contents)
=> ((delegate* unmanaged<Texture*, void*, bool>)_textureInitializeContents)(texture, contents);
public static void IncRef(Texture* texture)
=> ((delegate* unmanaged<Texture*, void>)(*(void***)texture)[2])(texture);
public static void DecRef(Texture* texture)
=> ((delegate* unmanaged<Texture*, void>)(*(void***)texture)[3])(texture);
}

View file

@ -65,6 +65,13 @@ public unsafe class ImcFile : MetaBaseFile
return ptr == null ? new ImcEntry() : *ptr;
}
public ImcEntry GetEntry(int partIdx, Variant variantIdx, out bool exists)
{
var ptr = VariantPtr(Data, partIdx, variantIdx);
exists = ptr != null;
return exists ? *ptr : new ImcEntry();
}
public static int PartIndex(EquipSlot slot)
=> slot switch
{
@ -161,11 +168,19 @@ public unsafe class ImcFile : MetaBaseFile
if (file == null)
throw new Exception();
fixed (byte* ptr = file.Data)
return GetEntry(file.Data, slot, variantIdx, out exists);
}
public static ImcEntry GetEntry(ReadOnlySpan<byte> imcFileData, EquipSlot slot, Variant variantIdx, out bool exists)
{
fixed (byte* ptr = imcFileData)
{
var entry = VariantPtr(ptr, PartIndex(slot), variantIdx);
if (entry == null)
{
exists = false;
return new ImcEntry();
}
exists = true;
return *entry;

View file

@ -76,7 +76,7 @@ public class Penumbra : IDalamudPlugin
_communicatorService = _services.GetRequiredService<CommunicatorService>();
_services.GetRequiredService<ResourceService>(); // Initialize because not required anywhere else.
_services.GetRequiredService<ModCacheManager>(); // Initialize because not required anywhere else.
_services.GetRequiredService<TextureUtility>(); // Initialize because not required anywhere else.
_services.GetRequiredService<ModelResourceHandleUtility>(); // Initialize because not required anywhere else.
_collectionManager.Caches.CreateNecessaryCaches();
using (var t = _services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
{

View file

@ -91,7 +91,7 @@ public static class ServiceManager
.AddSingleton<ResidentResourceManager>()
.AddSingleton<FontReloader>()
.AddSingleton<RedrawService>()
.AddSingleton<TextureUtility>();
.AddSingleton<ModelResourceHandleUtility>();
private static IServiceCollection AddConfiguration(this IServiceCollection services)
=> services.AddTransient<ConfigMigrationService>()

View file

@ -713,8 +713,8 @@ public class DebugTab : Window, ITab
UiHelpers.Text(resource);
ImGui.TableNextColumn();
var data = (nint)ResourceHandle.GetData(resource);
var length = ResourceHandle.GetLength(resource);
var data = (nint)resource->CsHandle.GetData();
var length = resource->CsHandle.GetLength();
if (ImGui.Selectable($"0x{data:X}"))
if (data != nint.Zero && length > 0)
ImGui.SetClipboardText(string.Join("\n",

View file

@ -99,10 +99,10 @@ public class ResourceTab : ITab
UiHelpers.Text(resource);
if (ImGui.IsItemClicked())
{
var data = Interop.Structs.ResourceHandle.GetData(resource);
var data = resource->CsHandle.GetData();
if (data != null)
{
var length = (int)Interop.Structs.ResourceHandle.GetLength(resource);
var length = (int)resource->CsHandle.GetLength();
ImGui.SetClipboardText(string.Join(" ",
new ReadOnlySpan<byte>(data, length).ToArray().Select(b => b.ToString("X2"))));
}

View file

@ -19,9 +19,18 @@ public static class UiHelpers
public static unsafe void Text(byte* s, int length)
=> ImGuiNative.igTextUnformatted(s, s + length);
/// <summary> Draw text given by a byte span. </summary>
public static unsafe void Text(ReadOnlySpan<byte> s)
{
fixed (byte* pS = s)
{
Text(pS, s.Length);
}
}
/// <summary> Draw the name of a resource file. </summary>
public static unsafe void Text(ResourceHandle* resource)
=> Text(resource->FileName().Path, resource->FileNameLength);
=> Text(resource->CsHandle.FileName.AsSpan());
/// <summary> Draw a ByteString as a selectable. </summary>
public static unsafe bool Selectable(ByteString s, bool selected)