mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Merge branch 'rt-rework'
This commit is contained in:
commit
806561b95a
34 changed files with 803 additions and 507 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit f39a716ad4f908c301d497728ede047ee6bd61c0
|
||||
Subproject commit ffdb966fec5a657893289e655c641ceb3af1d59f
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -30,11 +30,15 @@ public unsafe class PathState : IDisposable
|
|||
private readonly ResolvePathHooks _demiHuman;
|
||||
private readonly ResolvePathHooks _monster;
|
||||
|
||||
private readonly ThreadLocal<ResolveData> _resolveData = new(() => ResolveData.Invalid, true);
|
||||
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;
|
||||
|
||||
_resolveData.Value = collection.ToResolveData(gameObject);
|
||||
if (!InInternalResolve)
|
||||
{
|
||||
_resolveData.Value = collection.ToResolveData(gameObject);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +98,37 @@ public unsafe class PathState : IDisposable
|
|||
if (path == nint.Zero)
|
||||
return path;
|
||||
|
||||
_resolveData.Value = data;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
312
Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs
Normal file
312
Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed);
|
||||
prefixed[lastDirectorySeparator + 1] = (byte)'-';
|
||||
prefixed[lastDirectorySeparator + 2] = (byte)'-';
|
||||
gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]);
|
||||
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))
|
||||
return null;
|
||||
if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], out var tmp))
|
||||
return null;
|
||||
|
||||
gamePath = tmp.Path.Clone();
|
||||
}
|
||||
path = tmp.Clone();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (!Utf8GamePath.FromByteString(gamePath, out var path))
|
||||
return null;
|
||||
|
||||
return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal);
|
||||
return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path);
|
||||
}
|
||||
|
||||
private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle,
|
||||
Utf8GamePath gamePath, bool @internal)
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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 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
|
||||
);
|
||||
var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType);
|
||||
|
||||
for (var i = 0; i < subObject->SlotCount; ++i)
|
||||
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
||||
var imcNode = slotContext.CreateNodeFromImc(imc);
|
||||
if (imcNode != null)
|
||||
{
|
||||
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
||||
var imcNode = subObjectContext.CreateNodeFromImc(imc);
|
||||
if (imcNode != null)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}";
|
||||
subObjectNodes.Add(imcNode);
|
||||
}
|
||||
|
||||
var mdl = (RenderModel*)subObject->Models[i];
|
||||
var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl);
|
||||
if (mdlNode != null)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}";
|
||||
subObjectNodes.Add(mdlNode);
|
||||
}
|
||||
if (globalContext.WithUiData)
|
||||
imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}";
|
||||
weaponNodes.Add(imcNode);
|
||||
}
|
||||
|
||||
AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, ");
|
||||
var mdl = subObject->Models[i];
|
||||
var mdlNode = slotContext.CreateNodeFromModel(mdl, imc);
|
||||
if (mdlNode != null)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
||||
weaponNodes.Add(mdlNode);
|
||||
}
|
||||
}
|
||||
|
||||
subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject;
|
||||
++subObjectIndex;
|
||||
} while (subObject != null && subObject != firstSubObject);
|
||||
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, ");
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
tree.LoadResources(globalContext);
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
62
Penumbra/Interop/Structs/CharacterBaseUtility.cs
Normal file
62
Penumbra/Interop/Structs/CharacterBaseUtility.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
18
Penumbra/Interop/Structs/ModelResourceHandleUtility.cs
Normal file
18
Penumbra/Interop/Structs/ModelResourceHandleUtility.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
24
Penumbra/Interop/Structs/StructExtensions.cs
Normal file
24
Penumbra/Interop/Structs/StructExtensions.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"))));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue