diff --git a/Penumbra.GameData b/Penumbra.GameData index f39a716a..ffdb966f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f39a716ad4f908c301d497728ede047ee6bd61c0 +Subproject commit ffdb966fec5a657893289e655c641ceb3af1d59f diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 43ebcf56..9e2cdef9 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -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(); diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 8eb7a5a0..d5acf249 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -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 _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); + /// Use this when CharacterUtility becomes ready. private void ApplyStoredManipulations() { diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index bacc72fa..0b7bafe0 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -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; diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index fa03ac49..972d81be 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -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 value) diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index c64e4d0b..ec0ddd29 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -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)); } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 0048dc8c..c1e0bb80 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -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)) diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index f300a666..6d7840d8 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -30,11 +30,15 @@ public unsafe class PathState : IDisposable private readonly ResolvePathHooks _demiHuman; private readonly ResolvePathHooks _monster; - private readonly ThreadLocal _resolveData = new(() => ResolveData.Invalid, true); + private readonly ThreadLocal _resolveData = new(() => ResolveData.Invalid, true); + private readonly ThreadLocal _internalResolve = new(() => 0, false); public IList 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; } + + /// + /// Temporarily disables metadata mod application and resolve data capture on the current thread. + /// Must be called to prevent race conditions between Penumbra's internal path resolution (for example for Resource Trees) and the game's path resolution. + /// Please note that this will make path resolution cases that depend on metadata incorrect. + /// + /// A struct that will undo this operation when disposed. Best used with: using (var _ = pathState.EnterInternalResolve()) { ... } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public InternalResolveRaii EnterInternalResolve() + => new(this); + + public readonly ref struct InternalResolveRaii + { + private readonly ThreadLocal _internalResolve; + + public InternalResolveRaii(PathState parent) + { + _internalResolve = parent._internalResolve; + ++_internalResolve.Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public readonly void Dispose() + { + --_internalResolve.Value; + } + } } diff --git a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs index 9d010d64..3be7ffdd 100644 --- a/Penumbra/Interop/PathResolving/ResolvePathHooks.cs +++ b/Penumbra/Interop/PathResolving/ResolvePathHooks.cs @@ -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), diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs new file mode 100644 index 00000000..1c9dfaa1 --- /dev/null +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -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 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 mirroredFileName = stackalloc byte[32]; + mirroredFileName = mirroredFileName[..fileName.Length]; + fileName.CopyTo(mirroredFileName); + WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); + + Span 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 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 AssembleMaterialPath(Span materialPathBuffer, ReadOnlySpan modelPath, byte variant, ReadOnlySpan mtrlFileName) + { + var modelPosition = modelPath.IndexOf("/model/"u8); + if (modelPosition < 0) + return Span.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 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; + } +} diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 55893cab..73abcb4d 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -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 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 Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal partial record ResolveContext(GlobalResolveContext Global, Pointer 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 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 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 alreadyVisitedSamplerIds) { @@ -169,55 +198,55 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree } static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet 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(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(); - 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 FilterGamePaths(IReadOnlyCollection gamePaths) - { - var filtered = new List(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 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 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(); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index dfca5805..53dedfa0 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -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 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(); Length = length; - Internal = @internal; Children = new List(); 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; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index bc2cca26..bd994242 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -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(&character->DrawData.Head, 10); - // var customize = new ReadOnlySpan( 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(&human->Head, 10), + CharacterBase.ModelType.DemiHuman => new ReadOnlySpan(&character->DrawData.Head, 10), + _ => ReadOnlySpan.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(); + foreach (var baseSubObject in model->DrawObject.Object.ChildObjects) { - var subObjectNodes = new List(); - 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 nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") + private unsafe void AddSkeleton(List 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); } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 6353d5b5..0e3a92e2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -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 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) { diff --git a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs index df97371b..fd020804 100644 --- a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs +++ b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs @@ -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; } diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs index be5b778e..d25a5638 100644 --- a/Penumbra/Interop/Services/SkinFixer.cs +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -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 diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs deleted file mode 100644 index 53fda2cd..00000000 --- a/Penumbra/Interop/Structs/CharacterBaseExt.cs +++ /dev/null @@ -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; -} diff --git a/Penumbra/Interop/Structs/CharacterBaseUtility.cs b/Penumbra/Interop/Structs/CharacterBaseUtility.cs new file mode 100644 index 00000000..c29f44a3 --- /dev/null +++ b/Penumbra/Interop/Structs/CharacterBaseUtility.cs @@ -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)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)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)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); +} diff --git a/Penumbra/Interop/Structs/ConstantBuffer.cs b/Penumbra/Interop/Structs/ConstantBuffer.cs deleted file mode 100644 index d61aaeea..00000000 --- a/Penumbra/Interop/Structs/ConstantBuffer.cs +++ /dev/null @@ -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 buffer) - { - if ((Flags & 0x4003) == 0 && _maybeSourcePointer != null) - { - buffer = new Span(_maybeSourcePointer, Size >> 2); - return true; - } - else - { - buffer = null; - return false; - } - } -} diff --git a/Penumbra/Interop/Structs/HumanExt.cs b/Penumbra/Interop/Structs/HumanExt.cs deleted file mode 100644 index 274b4fb2..00000000 --- a/Penumbra/Interop/Structs/HumanExt.cs +++ /dev/null @@ -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; -} diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs deleted file mode 100644 index f7c8679e..00000000 --- a/Penumbra/Interop/Structs/Material.cs +++ /dev/null @@ -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 TextureSpan - => new(Textures, TextureCount); -} diff --git a/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs b/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs new file mode 100644 index 00000000..008cd59a --- /dev/null +++ b/Penumbra/Interop/Structs/ModelResourceHandleUtility.cs @@ -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)_getMaterialFileNameBySlot)(handle, slot); +} diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs deleted file mode 100644 index c3b86e14..00000000 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ /dev/null @@ -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; - } -} diff --git a/Penumbra/Interop/Structs/RenderModel.cs b/Penumbra/Interop/Structs/RenderModel.cs index f9cb2d56..86b09e8d 100644 --- a/Penumbra/Interop/Structs/RenderModel.cs +++ b/Penumbra/Interop/Structs/RenderModel.cs @@ -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; diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 3cceb949..382368b4 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -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 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. diff --git a/Penumbra/Interop/Structs/ShaderPackageUtility.cs b/Penumbra/Interop/Structs/ShaderPackageUtility.cs deleted file mode 100644 index 9f7ec1f5..00000000 --- a/Penumbra/Interop/Structs/ShaderPackageUtility.cs +++ /dev/null @@ -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; - } -} diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs new file mode 100644 index 00000000..d1a38ae4 --- /dev/null +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -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 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); +} diff --git a/Penumbra/Interop/Structs/TextureUtility.cs b/Penumbra/Interop/Structs/TextureUtility.cs deleted file mode 100644 index eeea4c33..00000000 --- a/Penumbra/Interop/Structs/TextureUtility.cs +++ /dev/null @@ -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)_textureCreate2D)(device, size, mipLevel, textureFormat, - flags, unk); - - public static bool InitializeContents(Texture* texture, void* contents) - => ((delegate* unmanaged)_textureInitializeContents)(texture, contents); - - public static void IncRef(Texture* texture) - => ((delegate* unmanaged)(*(void***)texture)[2])(texture); - - public static void DecRef(Texture* texture) - => ((delegate* unmanaged)(*(void***)texture)[3])(texture); -} diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 94bc2428..68d3f5b3 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -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 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; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 99a81fd1..d7daaf70 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -76,7 +76,7 @@ public class Penumbra : IDalamudPlugin _communicatorService = _services.GetRequiredService(); _services.GetRequiredService(); // Initialize because not required anywhere else. _services.GetRequiredService(); // Initialize because not required anywhere else. - _services.GetRequiredService(); // Initialize because not required anywhere else. + _services.GetRequiredService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) { diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 84f89f6d..2c4f385d 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -91,7 +91,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static IServiceCollection AddConfiguration(this IServiceCollection services) => services.AddTransient() diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index dc39707a..c5811051 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -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", diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 020493d1..6f3dec30 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -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(data, length).ToArray().Select(b => b.ToString("X2")))); } diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 6c64bd55..8fbce6d0 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -19,9 +19,18 @@ public static class UiHelpers public static unsafe void Text(byte* s, int length) => ImGuiNative.igTextUnformatted(s, s + length); + /// Draw text given by a byte span. + public static unsafe void Text(ReadOnlySpan s) + { + fixed (byte* pS = s) + { + Text(pS, s.Length); + } + } + /// Draw the name of a resource file. public static unsafe void Text(ResourceHandle* resource) - => Text(resource->FileName().Path, resource->FileNameLength); + => Text(resource->CsHandle.FileName.AsSpan()); /// Draw a ByteString as a selectable. public static unsafe bool Selectable(ByteString s, bool selected)