From 00dc5f48b1c32da73f4d8d10c60e9a9c698ff7d0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 27 Oct 2023 01:52:16 +0200 Subject: [PATCH 01/15] ClientStructs-ify a few things --- OtterGui | 2 +- .../LiveColorTablePreviewer.cs | 8 +-- .../MaterialPreview/LiveMaterialPreviewer.cs | 4 +- .../Interop/MaterialPreview/MaterialInfo.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 47 +++++++------- Penumbra/Interop/ResourceTree/ResourceTree.cs | 21 ++++--- .../Interop/SafeHandles/SafeTextureHandle.cs | 5 +- Penumbra/Interop/Services/SkinFixer.cs | 13 ++-- Penumbra/Interop/Structs/CharacterBaseExt.cs | 14 ----- Penumbra/Interop/Structs/ConstantBuffer.cs | 28 --------- Penumbra/Interop/Structs/HumanExt.cs | 19 ------ Penumbra/Interop/Structs/Material.cs | 47 -------------- Penumbra/Interop/Structs/MtrlResource.cs | 45 ------------- Penumbra/Interop/Structs/RenderModel.cs | 3 + Penumbra/Interop/Structs/ResourceHandle.cs | 63 +++---------------- .../Interop/Structs/ShaderPackageUtility.cs | 17 ----- Penumbra/Interop/Structs/StructExtensions.cs | 24 +++++++ Penumbra/Interop/Structs/TextureUtility.cs | 31 --------- Penumbra/Penumbra.cs | 1 - Penumbra/Services/ServiceManager.cs | 3 +- Penumbra/UI/Tabs/DebugTab.cs | 4 +- Penumbra/UI/Tabs/ResourceTab.cs | 4 +- Penumbra/UI/UiHelpers.cs | 11 +++- 23 files changed, 102 insertions(+), 314 deletions(-) delete mode 100644 Penumbra/Interop/Structs/CharacterBaseExt.cs delete mode 100644 Penumbra/Interop/Structs/ConstantBuffer.cs delete mode 100644 Penumbra/Interop/Structs/HumanExt.cs delete mode 100644 Penumbra/Interop/Structs/Material.cs delete mode 100644 Penumbra/Interop/Structs/MtrlResource.cs delete mode 100644 Penumbra/Interop/Structs/ShaderPackageUtility.cs create mode 100644 Penumbra/Interop/Structs/StructExtensions.cs delete mode 100644 Penumbra/Interop/Structs/TextureUtility.cs diff --git a/OtterGui b/OtterGui index a4f9b285..6f17ef70 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a4f9b285c82f84ff0841695c0787dbba93afc59b +Subproject commit 6f17ef70c41f3b31a401fdc9d6e37087e64f2035 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/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 55893cab..5e3970cc 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,14 +1,15 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui; using Penumbra.Api.Enums; 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.StructExtensions; namespace Penumbra.Interop.ResourceTree; @@ -36,7 +37,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree 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 CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path, @internal); } private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11) @@ -68,7 +69,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (!Utf8GamePath.FromByteString(gamePath, out var path)) return null; - return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal); + return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path, @internal); } private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, @@ -118,22 +119,22 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false); + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, false); if (node != null) Nodes.Add((nint)tex, node); return node; } - public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl) + public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl) { - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + if (mdl == null || mdl->ModelResourceHandle == null || mdl->ModelResourceHandle->ResourceHandle.Type.Category != ResourceHandleType.HandleCategory.Chara) return null; - if (Nodes.TryGetValue((nint)mdl->ResourceHandle, out var cached)) + if (Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, mdl->ResourceHandle, false); + var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, false); if (node == null) return null; @@ -149,7 +150,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree } } - Nodes.Add((nint)mdl->ResourceHandle, node); + Nodes.Add((nint)mdl->ModelResourceHandle, node); return node; } @@ -169,27 +170,27 @@ 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) return null; - var resource = mtrl->ResourceHandle; + var resource = mtrl->MaterialResourceHandle; if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->Handle, false); + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, 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), false); if (shpkNode != null) { if (WithUiData) @@ -200,10 +201,10 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree var shpk = 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)), false, + resource->Textures[i].IsDX11); if (texNode == null) continue; @@ -212,12 +213,12 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree 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); @@ -367,7 +368,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (handle == null) return ByteString.Empty; - var name = handle->FileName(); + var name = handle->FileName.AsByteString(); if (name.IsEmpty) return ByteString.Empty; @@ -388,6 +389,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/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index bc2cca26..53e7db35 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,9 +1,9 @@ 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.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; @@ -50,11 +50,12 @@ public class ResourceTree { var character = (Character*)GameObjectAddress; var model = (CharacterBase*)DrawObjectAddress; + var human = model->GetModelType() == CharacterBase.ModelType.Human ? (Human*)model : null; var equipment = new ReadOnlySpan(&character->DrawData.Head, 10); // var customize = new ReadOnlySpan( character->CustomizeData, 26 ); 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; for (var i = 0; i < model->SlotCount; ++i) { @@ -72,7 +73,7 @@ public class ResourceTree Nodes.Add(imcNode); } - var mdl = (RenderModel*)model->Models[i]; + var mdl = model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) { @@ -84,13 +85,13 @@ public class ResourceTree AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); - if (model->GetModelType() == CharacterBase.ModelType.Human) - AddHumanResources(globalContext, (HumanExt*)model); + if (human != null) + AddHumanResources(globalContext, human); } - private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human) + private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) { - var firstSubObject = (CharacterBase*)human->Human.CharacterBase.DrawObject.Object.ChildObject; + var firstSubObject = (CharacterBase*)human->CharacterBase.DrawObject.Object.ChildObject; if (firstSubObject != null) { var subObjectNodes = new List(); @@ -116,7 +117,7 @@ public class ResourceTree subObjectNodes.Add(imcNode); } - var mdl = (RenderModel*)subObject->Models[i]; + var mdl = subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) { @@ -137,7 +138,7 @@ public class ResourceTree var context = globalContext.CreateContext(EquipSlot.Unknown, default); - var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); + var decalNode = context.CreateNodeFromTex(human->Decal); if (decalNode != null) { if (globalContext.WithUiData) @@ -149,7 +150,7 @@ public class ResourceTree Nodes.Add(decalNode); } - var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); + var legacyDecalNode = context.CreateNodeFromTex(human->LegacyBodyDecal); if (legacyDecalNode != null) { if (globalContext.WithUiData) 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/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/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/Penumbra.cs b/Penumbra/Penumbra.cs index 73d1013e..ce1bdae5 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -75,7 +75,6 @@ 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. _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..6a522ca2 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -90,8 +90,7 @@ public static class ServiceManager .AddSingleton() .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 5abb3c2f..1c5f7946 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -669,8 +669,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) From 5085aa500c167bba4853346a7e4a651129a7d54d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 27 Oct 2023 02:13:57 +0200 Subject: [PATCH 02/15] ResourceTree: Use DrawObject as equipment source + CS-ify a bit more --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 53e7db35..836e79e2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -50,9 +50,14 @@ public class ResourceTree { var character = (Character*)GameObjectAddress; var model = (CharacterBase*)DrawObjectAddress; - var human = model->GetModelType() == CharacterBase.ModelType.Human ? (Human*)model : null; - 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 = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; @@ -91,50 +96,51 @@ public class ResourceTree private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) { - var firstSubObject = (CharacterBase*)human->CharacterBase.DrawObject.Object.ChildObject; - if (firstSubObject != null) + var subObjectIndex = 0; + var weaponIndex = 0; + var subObjectNodes = new List(); + foreach (var baseSubObject in human->CharacterBase.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; + + var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null; + var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc."; + // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. + var subObjectContext = globalContext.CreateContext( + weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown, + weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default + ); + + 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 - ); - - for (var i = 0; i < subObject->SlotCount; ++i) + var imc = (ResourceHandle*)subObject->IMCArray[i]; + var imcNode = subObjectContext.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 = 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 = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}"; + subObjectNodes.Add(imcNode); } - AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); + var mdl = subObject->Models[i]; + var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); + if (mdlNode != null) + { + if (globalContext.WithUiData) + mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}"; + subObjectNodes.Add(mdlNode); + } + } - subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject; - ++subObjectIndex; - } while (subObject != null && subObject != firstSubObject); + AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); - Nodes.InsertRange(0, subObjectNodes); + ++subObjectIndex; + if (weapon != null) + ++weaponIndex; } + Nodes.InsertRange(0, subObjectNodes); var context = globalContext.CreateContext(EquipSlot.Unknown, default); From db9bfb00a32cbac3fbae13460e7ba1f86559f39a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 27 Oct 2023 02:17:41 +0200 Subject: [PATCH 03/15] ResourceTree: Show SKP files out of Debug --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 5e3970cc..ade3ccd7 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -268,7 +268,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true); + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, false); if (node != null) { if (WithUiData) From 3da20f2d8971553050715bd94a046d0c1a839701 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 01:08:02 +0100 Subject: [PATCH 04/15] ResourceTree: Rework Internal flag, improve null checks, simplify --- .../Interop/ResourceTree/ResolveContext.cs | 91 +++++++++---------- Penumbra/Interop/ResourceTree/ResourceNode.cs | 8 +- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index ade3ccd7..8e286ad0 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -27,8 +27,11 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) + private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath) { + if (resourceHandle == null) + return null; + if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) return cached; @@ -37,11 +40,14 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) return null; - return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path, @internal); + return CreateNode(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 (resourceHandle == null) + return null; + if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) return cached; @@ -65,82 +71,73 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree gamePath = tmp.Path.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 var path)) return null; - return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path, @internal); + return CreateNode(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 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) + if (autoAdd) Nodes.Add((nint)resourceHandle, node); return node; } - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal) - { - var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; - if (fullPath.InternalName.IsEmpty) - return null; - - return new ResourceNode(type, objectAddress, (nint)handle, GetResourceHandleLength(handle), @internal, this) - { - FullPath = fullPath, - }; - } - public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { + if (imc == null) + return null; + if (Nodes.TryGetValue((nint)imc, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true); - if (node == null) - return null; - - Nodes.Add((nint)imc, node); - - return node; + return CreateNode(ResourceType.Imc, 0, imc, Utf8GamePath.Empty); } public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) { + if (tex == null) + return null; + if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, false); - if (node != null) - Nodes.Add((nint)tex, node); - - return node; + return CreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, Utf8GamePath.Empty); } public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl) { - if (mdl == null || mdl->ModelResourceHandle == null || mdl->ModelResourceHandle->ResourceHandle.Type.Category != ResourceHandleType.HandleCategory.Chara) + if (mdl == null || mdl->ModelResourceHandle == null) return null; if (Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, false); - if (node == null) - return null; + var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, Utf8GamePath.Empty, false); for (var i = 0; i < mdl->MaterialCount; i++) { - var mtrl = (Material*)mdl->Materials[i]; + var mtrl = mdl->Materials[i]; var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) { @@ -179,18 +176,18 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree ? s.CRC : null; - if (mtrl == null) + if (mtrl == null || mtrl->MaterialResourceHandle == null) return null; var resource = mtrl->MaterialResourceHandle; if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, false); + var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, Utf8GamePath.Empty, false); if (node == null) return null; - var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName), false); + var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName)); if (shpkNode != null) { if (WithUiData) @@ -203,7 +200,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) { - var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new ByteString(resource->TexturePath(i)), false, + var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new ByteString(resource->TexturePath(i)), resource->Textures[i].IsDX11); if (texNode == null) continue; @@ -240,15 +237,15 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree return node; } - public unsafe ResourceNode? CreateNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + public unsafe ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb) { - if (sklb->SkeletonResourceHandle == null) + if (sklb == null || sklb->SkeletonResourceHandle == null) return null; if (Nodes.TryGetValue((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, Utf8GamePath.Empty, false); if (node != null) { var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); @@ -260,15 +257,15 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree return node; } - private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb) + private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb) { - if (sklb->SkeletonParameterResourceHandle == null) + if (sklb == null || sklb->SkeletonParameterResourceHandle == null) return null; if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, false); + var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, Utf8GamePath.Empty, false); if (node != null) { if (WithUiData) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index dfca5805..f520c83a 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.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; } From 28a396470baae8e2162b94d2e0126b1d4e6b914d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 01:11:48 +0100 Subject: [PATCH 05/15] ResourceTree: De-inline GlobalResolveContext in ResolveContext --- .../Interop/ResourceTree/ResolveContext.cs | 49 +++++++++---------- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 8e286ad0..e5a122ac 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -19,11 +19,10 @@ internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCach public readonly Dictionary Nodes = new(128); public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Identifier, TreeBuildCache, Skeleton, WithUiData, Nodes, slot, equipment); + => new(this, slot, equipment); } -internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, int Skeleton, bool WithUiData, - Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); @@ -32,7 +31,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (resourceHandle == null) return null; - if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)resourceHandle, out var cached)) return cached; if (gamePath.IsEmpty) @@ -48,7 +47,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (resourceHandle == null) return null; - if (Nodes.TryGetValue((nint)resourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)resourceHandle, out var cached)) return cached; if (dx11) @@ -98,7 +97,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree FullPath = fullPath, }; if (autoAdd) - Nodes.Add((nint)resourceHandle, node); + Global.Nodes.Add((nint)resourceHandle, node); return node; } @@ -108,7 +107,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (imc == null) return null; - if (Nodes.TryGetValue((nint)imc, out var cached)) + if (Global.Nodes.TryGetValue((nint)imc, out var cached)) return cached; return CreateNode(ResourceType.Imc, 0, imc, Utf8GamePath.Empty); @@ -119,7 +118,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (tex == null) return null; - if (Nodes.TryGetValue((nint)tex, out var cached)) + if (Global.Nodes.TryGetValue((nint)tex, out var cached)) return cached; return CreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, Utf8GamePath.Empty); @@ -130,7 +129,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (mdl == null || mdl->ModelResourceHandle == null) return null; - if (Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) return cached; var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, Utf8GamePath.Empty, false); @@ -141,13 +140,13 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) { - if (WithUiData) + if (Global.WithUiData) mtrlNode.FallbackName = $"Material #{i}"; node.Children.Add(mtrlNode); } } - Nodes.Add((nint)mdl->ModelResourceHandle, node); + Global.Nodes.Add((nint)mdl->ModelResourceHandle, node); return node; } @@ -180,7 +179,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree return null; var resource = mtrl->MaterialResourceHandle; - if (Nodes.TryGetValue((nint)resource, out var cached)) + if (Global.Nodes.TryGetValue((nint)resource, out var cached)) return cached; var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, Utf8GamePath.Empty, false); @@ -190,12 +189,12 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree 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->TextureCount; i++) @@ -205,7 +204,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (texNode == null) continue; - if (WithUiData) + if (Global.WithUiData) { string? name = null; if (shpk != null) @@ -232,7 +231,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree node.Children.Add(texNode); } - Nodes.Add((nint)resource, node); + Global.Nodes.Add((nint)resource, node); return node; } @@ -242,7 +241,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (sklb == null || sklb->SkeletonResourceHandle == null) return null; - if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) return cached; var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, Utf8GamePath.Empty, false); @@ -251,7 +250,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); if (skpNode != null) node.Children.Add(skpNode); - Nodes.Add((nint)sklb->SkeletonResourceHandle, node); + Global.Nodes.Add((nint)sklb->SkeletonResourceHandle, node); } return node; @@ -262,15 +261,15 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree if (sklb == null || sklb->SkeletonParameterResourceHandle == null) return null; - if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) + if (Global.Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, Utf8GamePath.Empty, false); if (node != null) { - if (WithUiData) + if (Global.WithUiData) node.FallbackName = "Skeleton Parameters"; - Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); + Global.Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); } return node; @@ -297,7 +296,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree { "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}", + "monster" => SafeGet(path, 2) == $"m{Global.Skeleton:D4}", "weapon" => IsMatchEquipment(path[2..], $"w{Equipment.Set.Id:D4}"), _ => null, }, @@ -319,7 +318,7 @@ internal record ResolveContext(IObjectIdentifier Identifier, TreeBuildCache Tree // 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 { @@ -342,7 +341,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:")) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 836e79e2..38dae6b8 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -179,7 +179,7 @@ public class ResourceTree var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) { - if (context.WithUiData) + if (context.Global.WithUiData) sklbNode.FallbackName = $"{prefix}Skeleton #{i}"; nodes.Add(sklbNode); } From 57f8587a4358853f85be4a3d75a59f5df24e69c0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 01:18:20 +0100 Subject: [PATCH 06/15] ResourceTree: Use both game path and resource handle as keys for dedup --- .../Interop/ResourceTree/ResolveContext.cs | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index e5a122ac..a359411b 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -16,7 +16,7 @@ namespace Penumbra.Interop.ResourceTree; internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, int Skeleton, 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(this, slot, equipment); @@ -30,16 +30,12 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char { if (resourceHandle == null) return null; - - if (Global.Nodes.TryGetValue((nint)resourceHandle, out var cached)) - return cached; - if (gamePath.IsEmpty) return null; if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) return null; - return CreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); + return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); } private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11) @@ -47,9 +43,6 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (resourceHandle == null) return null; - if (Global.Nodes.TryGetValue((nint)resourceHandle, out var cached)) - return cached; - if (dx11) { var lastDirectorySeparator = gamePath.LastIndexOf((byte)'/'); @@ -80,7 +73,19 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (!Utf8GamePath.FromByteString(gamePath, out var path)) return null; - return CreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path); + return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path); + } + + private unsafe ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, + Utf8GamePath gamePath) + { + if (resourceHandle == null) + throw new ArgumentNullException(nameof(resourceHandle)); + + if (Global.Nodes.TryGetValue((gamePath, (nint)resourceHandle), out var cached)) + return cached; + + return CreateNode(type, objectAddress, resourceHandle, gamePath); } private unsafe ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, @@ -97,7 +102,7 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char FullPath = fullPath, }; if (autoAdd) - Global.Nodes.Add((nint)resourceHandle, node); + Global.Nodes.Add((gamePath, (nint)resourceHandle), node); return node; } @@ -107,10 +112,9 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (imc == null) return null; - if (Global.Nodes.TryGetValue((nint)imc, out var cached)) - return cached; + var path = Utf8GamePath.Empty; // TODO - return CreateNode(ResourceType.Imc, 0, imc, Utf8GamePath.Empty); + return GetOrCreateNode(ResourceType.Imc, 0, imc, path); } public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) @@ -118,10 +122,9 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (tex == null) return null; - if (Global.Nodes.TryGetValue((nint)tex, out var cached)) - return cached; + var path = Utf8GamePath.Empty; // TODO - return CreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, Utf8GamePath.Empty); + return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path); } public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl) @@ -129,10 +132,12 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (mdl == null || mdl->ModelResourceHandle == null) return null; - if (Global.Nodes.TryGetValue((nint)mdl->ModelResourceHandle, out var cached)) + var path = Utf8GamePath.Empty; // TODO + + if (Global.Nodes.TryGetValue((path, (nint)mdl->ModelResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, Utf8GamePath.Empty, false); + var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, path, false); for (var i = 0; i < mdl->MaterialCount; i++) { @@ -146,7 +151,7 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char } } - Global.Nodes.Add((nint)mdl->ModelResourceHandle, node); + Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node); return node; } @@ -178,11 +183,13 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (mtrl == null || mtrl->MaterialResourceHandle == null) return null; + var path = Utf8GamePath.Empty; // TODO + var resource = mtrl->MaterialResourceHandle; - if (Global.Nodes.TryGetValue((nint)resource, out var cached)) + if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached)) return cached; - var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, Utf8GamePath.Empty, false); + var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); if (node == null) return null; @@ -231,7 +238,7 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char node.Children.Add(texNode); } - Global.Nodes.Add((nint)resource, node); + Global.Nodes.Add((path, (nint)resource), node); return node; } @@ -241,16 +248,18 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (sklb == null || sklb->SkeletonResourceHandle == null) return null; - if (Global.Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) + var path = Utf8GamePath.Empty; // TODO + + if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, Utf8GamePath.Empty, false); + var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); if (node != null) { var skpNode = CreateParameterNodeFromPartialSkeleton(sklb); if (skpNode != null) node.Children.Add(skpNode); - Global.Nodes.Add((nint)sklb->SkeletonResourceHandle, node); + Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); } return node; @@ -261,15 +270,17 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (sklb == null || sklb->SkeletonParameterResourceHandle == null) return null; - if (Global.Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) + var path = Utf8GamePath.Empty; // TODO + + if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, Utf8GamePath.Empty, false); + var node = CreateNode(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, path, false); if (node != null) { if (Global.WithUiData) node.FallbackName = "Skeleton Parameters"; - Global.Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); + Global.Nodes.Add((path, (nint)sklb->SkeletonParameterResourceHandle), node); } return node; From da54222bb1f66ce189b335a7cbf78521a5a1583d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 20:59:09 +0100 Subject: [PATCH 07/15] ResourceTree: Add EID files --- .../Interop/ResourceTree/ResolveContext.cs | 10 ++++++++++ Penumbra/Interop/ResourceTree/ResourceNode.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index a359411b..26d64afe 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -107,6 +107,16 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char return node; } + public unsafe ResourceNode? CreateNodeFromEid(ResourceHandle* eid) + { + if (eid == null) + return null; + + var path = Utf8GamePath.Empty; // TODO + + return GetOrCreateNode(ResourceType.Eid, 0, eid, path); + } + public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { if (imc == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index f520c83a..53dedfa0 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -31,7 +31,7 @@ public class ResourceNode : ICloneable } public bool Internal - => Type is ResourceType.Imc; + => Type is ResourceType.Eid or ResourceType.Imc; internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext) { diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 38dae6b8..687c14ec 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -62,6 +62,15 @@ public class ResourceTree CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; + var eid = (ResourceHandle*)model->EID; + var eidNode = globalContext.CreateContext(EquipSlot.Unknown, default).CreateNodeFromEid(eid); + if (eidNode != null) + { + if (globalContext.WithUiData) + eidNode.FallbackName = "EID"; + Nodes.Add(eidNode); + } + for (var i = 0; i < model->SlotCount; ++i) { var context = globalContext.CreateContext( @@ -113,6 +122,15 @@ public class ResourceTree weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default ); + var eid = (ResourceHandle*)subObject->EID; + var eidNode = subObjectContext.CreateNodeFromEid(eid); + if (eidNode != null) + { + if (globalContext.WithUiData) + eidNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, EID"; + Nodes.Add(eidNode); + } + for (var i = 0; i < subObject->SlotCount; ++i) { var imc = (ResourceHandle*)subObject->IMCArray[i]; From 2852562a03d4a09043691eeb2f5dad28ba956c30 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 2 Nov 2023 22:41:20 +0100 Subject: [PATCH 08/15] ResourceTree: Use ResolveXXXPath where possible --- Penumbra.GameData | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 35 +++++++---- Penumbra/Interop/ResourceTree/ResourceTree.cs | 59 ++++++++++++------ .../Interop/Structs/CharacterBaseUtility.cs | 62 +++++++++++++++++++ 4 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 Penumbra/Interop/Structs/CharacterBaseUtility.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 04ddadb4..b141301c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 04ddadb44600a382e26661e1db08fd16c3b671d8 +Subproject commit b141301c4ee65422d6802f3038c8f344911d4ae2 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 26d64afe..d700131d 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,6 +1,8 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; 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.GameData; @@ -9,6 +11,7 @@ using Penumbra.GameData.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; +using static Penumbra.Interop.Structs.CharacterBaseUtility; using static Penumbra.Interop.Structs.StructExtensions; namespace Penumbra.Interop.ResourceTree; @@ -18,11 +21,11 @@ internal record GlobalResolveContext(IObjectIdentifier Identifier, TreeBuildCach { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); - public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(this, slot, equipment); + public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex, EquipSlot slot, CharacterArmor equipment) + => new(this, characterBase, slotIndex, slot, equipment); } -internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, CharacterArmor Equipment) +internal record ResolveContext(GlobalResolveContext Global, Pointer CharacterBase, uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); @@ -112,7 +115,8 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (eid == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveEidPath(CharacterBase), out var path)) + return null; return GetOrCreateNode(ResourceType.Eid, 0, eid, path); } @@ -122,17 +126,19 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (imc == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveImcPath(CharacterBase, SlotIndex), out var path)) + return null; return GetOrCreateNode(ResourceType.Imc, 0, imc, path); } - public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex) + public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath) { if (tex == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromString(gamePath, out var path)) + return null; return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path); } @@ -142,7 +148,8 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char if (mdl == null || mdl->ModelResourceHandle == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path)) + return null; if (Global.Nodes.TryGetValue((path, (nint)mdl->ModelResourceHandle), out var cached)) return cached; @@ -253,12 +260,13 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char return node; } - public unsafe ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb) + public unsafe ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) { if (sklb == null || sklb->SkeletonResourceHandle == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveSklbPath(CharacterBase, partialSkeletonIndex), out var path)) + return null; if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; @@ -266,7 +274,7 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char 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); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); @@ -275,12 +283,13 @@ internal record ResolveContext(GlobalResolveContext Global, EquipSlot Slot, Char return node; } - private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb) + private unsafe ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) { if (sklb == null || sklb->SkeletonParameterResourceHandle == null) return null; - var path = Utf8GamePath.Empty; // TODO + if (!Utf8GamePath.FromByteString(ResolveSkpPath(CharacterBase, partialSkeletonIndex), out var path)) + return null; if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached)) return cached; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 687c14ec..7c58d6a8 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,7 +1,9 @@ +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.UI; @@ -62,8 +64,10 @@ public class ResourceTree CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; + var genericContext = globalContext.CreateContext(model, 0xFFFFFFFFu, EquipSlot.Unknown, default); + var eid = (ResourceHandle*)model->EID; - var eidNode = globalContext.CreateContext(EquipSlot.Unknown, default).CreateNodeFromEid(eid); + var eidNode = genericContext.CreateNodeFromEid(eid); if (eidNode != null) { if (globalContext.WithUiData) @@ -73,13 +77,15 @@ public class ResourceTree for (var i = 0; i < model->SlotCount; ++i) { - var context = globalContext.CreateContext( + var slotContext = globalContext.CreateContext( + model, + (uint)i, i < equipment.Length ? ((uint)i).ToEquipSlot() : EquipSlot.Unknown, i < equipment.Length ? equipment[i] : default ); var imc = (ResourceHandle*)model->IMCArray[i]; - var imcNode = context.CreateNodeFromImc(imc); + var imcNode = slotContext.CreateNodeFromImc(imc); if (imcNode != null) { if (globalContext.WithUiData) @@ -88,7 +94,7 @@ public class ResourceTree } var mdl = model->Models[i]; - var mdlNode = context.CreateNodeFromRenderModel(mdl); + var mdlNode = slotContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) { if (globalContext.WithUiData) @@ -97,18 +103,20 @@ public class ResourceTree } } - AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); + AddSkeleton(Nodes, genericContext, model->Skeleton); + + AddSubObjects(globalContext, model); if (human != null) AddHumanResources(globalContext, human); } - private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) + private unsafe void AddSubObjects(GlobalResolveContext globalContext, CharacterBase* model) { var subObjectIndex = 0; var weaponIndex = 0; var subObjectNodes = new List(); - foreach (var baseSubObject in human->CharacterBase.DrawObject.Object.ChildObjects) + foreach (var baseSubObject in model->DrawObject.Object.ChildObjects) { if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) continue; @@ -117,13 +125,13 @@ public class ResourceTree var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null; var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc."; // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. - var subObjectContext = globalContext.CreateContext( - weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown, - weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default - ); + var slot = weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown; + var equipment = weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default; + + var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment); var eid = (ResourceHandle*)subObject->EID; - var eidNode = subObjectContext.CreateNodeFromEid(eid); + var eidNode = genericContext.CreateNodeFromEid(eid); if (eidNode != null) { if (globalContext.WithUiData) @@ -133,8 +141,10 @@ public class ResourceTree for (var i = 0; i < subObject->SlotCount; ++i) { + var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment); + var imc = (ResourceHandle*)subObject->IMCArray[i]; - var imcNode = subObjectContext.CreateNodeFromImc(imc); + var imcNode = slotContext.CreateNodeFromImc(imc); if (imcNode != null) { if (globalContext.WithUiData) @@ -143,7 +153,7 @@ public class ResourceTree } var mdl = subObject->Models[i]; - var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); + var mdlNode = slotContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) { if (globalContext.WithUiData) @@ -152,17 +162,24 @@ public class ResourceTree } } - AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); + AddSkeleton(subObjectNodes, genericContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); ++subObjectIndex; if (weapon != null) ++weaponIndex; } Nodes.InsertRange(0, subObjectNodes); + } - var context = globalContext.CreateContext(EquipSlot.Unknown, default); + private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) + { + var genericContext = globalContext.CreateContext(&human->CharacterBase, 0xFFFFFFFFu, EquipSlot.Unknown, default); - var decalNode = context.CreateNodeFromTex(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) @@ -174,7 +191,11 @@ public class ResourceTree Nodes.Add(decalNode); } - var legacyDecalNode = context.CreateNodeFromTex(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) @@ -194,7 +215,7 @@ public class ResourceTree 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.Global.WithUiData) 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); +} From fd163f8f66b1bc6edaa9a0cb2a431dcb94eef29d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 4 Nov 2023 18:30:36 +0100 Subject: [PATCH 09/15] ResourceTree: WIP - Path resolution --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/EstCache.cs | 21 ++ Penumbra/Collections/Cache/MetaCache.cs | 18 ++ Penumbra/Interop/PathResolving/PathState.cs | 44 +++- .../Interop/PathResolving/ResolvePathHooks.cs | 6 +- .../ResolveContext.PathResolution.cs | 248 ++++++++++++++++++ .../Interop/ResourceTree/ResolveContext.cs | 105 +++----- Penumbra/Interop/ResourceTree/ResourceTree.cs | 88 +++---- .../ResourceTree/ResourceTreeFactory.cs | 33 ++- .../Structs/ModelResourceHandleUtility.cs | 18 ++ Penumbra/Meta/Files/ImcFile.cs | 7 + Penumbra/Penumbra.cs | 1 + Penumbra/Services/ServiceManager.cs | 3 +- 13 files changed, 452 insertions(+), 142 deletions(-) create mode 100644 Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs create mode 100644 Penumbra/Interop/Structs/ModelResourceHandleUtility.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index b141301c..1f274b41 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b141301c4ee65422d6802f3038c8f344911d4ae2 +Subproject commit 1f274b41e3e703712deb83f3abd8727e10614ebe 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..0da11022 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,23 @@ public class MetaCache : IDisposable, IEnumerable _imcCache.GetImcFile(path, out file); + public ImcEntry GetImcEntry(Utf8GamePath path, EquipSlot slot, Variant variantIdx, out bool exists) + => GetImcFile(path, out var file) + ? file.GetEntry(Meta.Files.ImcFile.PartIndex(slot), variantIdx, out exists) + : Meta.Files.ImcFile.GetDefault(_manager, path, slot, variantIdx, out exists); + + 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/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..bcc957df --- /dev/null +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -0,0 +1,248 @@ +using Dalamud.Game.ClientState.Objects.Enums; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +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, Utf8GamePath imcPath, byte* mtrlFileName) + { + // Safety: + // Resolving a material path through the game's code can dereference null pointers for equipment materials. + return ModelType switch + { + ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), + ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), + ModelType.Weapon => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), + _ => ResolveMaterialPathNative(mtrlFileName), + }; + } + + private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + { + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); + var modelPathSpan = modelPath.Path.Span; + var baseDirectory = modelPathSpan[..modelPathSpan.IndexOf("/model/"u8)]; + + var variant = ResolveMaterialVariant(imcPath); + + Span pathBuffer = stackalloc byte[260]; + baseDirectory.CopyTo(pathBuffer); + "/material/v"u8.CopyTo(pathBuffer[baseDirectory.Length..]); + WriteZeroPaddedNumber(pathBuffer.Slice(baseDirectory.Length + 11, 4), variant); + pathBuffer[baseDirectory.Length + 15] = (byte)'/'; + fileName.CopyTo(pathBuffer[(baseDirectory.Length + 16)..]); + + return Utf8GamePath.FromSpan(pathBuffer[..(baseDirectory.Length + 16 + fileName.Length)], out var path) ? path.Clone() : Utf8GamePath.Empty; + } + + private byte ResolveMaterialVariant(Utf8GamePath imcPath) + { + var metaCache = Global.Collection.MetaCache; + if (metaCache == null) + return Equipment.Variant.Id; + + var entry = metaCache.GetImcEntry(imcPath, Slot, Equipment.Variant, out var exists); + if (!exists) + return Equipment.Variant.Id; + + return entry.MaterialId; + } + + 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 d700131d..f34a6ae2 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -5,6 +5,7 @@ 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; @@ -12,23 +13,29 @@ 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<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); - public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex, EquipSlot slot, CharacterArmor equipment) - => new(this, characterBase, slotIndex, 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(GlobalResolveContext Global, Pointer CharacterBase, uint SlotIndex, 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 ModelType ModelType + => CharacterBase.Value->GetModelType(); + private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath) { if (resourceHandle == null) @@ -46,35 +53,33 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer 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 var path)) - return null; + if (!Utf8GamePath.FromByteString(gamePath, out path)) + return null; + } return GetOrCreateNode(ResourceType.Tex, (nint)resourceHandle->Texture, &resourceHandle->ResourceHandle, path); } @@ -143,23 +148,28 @@ internal record ResolveContext(GlobalResolveContext Global, PointerTexture, &tex->ResourceHandle, path); } - public unsafe ResourceNode? CreateNodeFromRenderModel(Model* mdl) + public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, Utf8GamePath imcPath) { if (mdl == null || mdl->ModelResourceHandle == null) return null; + var mdlResource = mdl->ModelResourceHandle; if (!Utf8GamePath.FromByteString(ResolveMdlPath(CharacterBase, SlotIndex), out var path)) return null; - if (Global.Nodes.TryGetValue((path, (nint)mdl->ModelResourceHandle), out var cached)) + if (Global.Nodes.TryGetValue((path, (nint)mdlResource), out var cached)) return cached; - var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdl->ModelResourceHandle->ResourceHandle, path, false); + var node = CreateNode(ResourceType.Mdl, (nint)mdl, &mdlResource->ResourceHandle, path, false); for (var i = 0; i < mdl->MaterialCount; i++) { - var mtrl = 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, imcPath, mtrlFileName)); if (mtrlNode != null) { if (Global.WithUiData) @@ -173,7 +183,7 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer alreadyVisitedSamplerIds) { @@ -200,8 +210,6 @@ internal record ResolveContext(GlobalResolveContext Global, PointerMaterialResourceHandle == null) return null; - var path = Utf8GamePath.Empty; // TODO - var resource = mtrl->MaterialResourceHandle; if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached)) return cached; @@ -265,8 +273,7 @@ internal record ResolveContext(GlobalResolveContext Global, PointerSkeletonResourceHandle == null) return null; - if (!Utf8GamePath.FromByteString(ResolveSklbPath(CharacterBase, partialSkeletonIndex), out var path)) - return null; + var path = ResolveSkeletonPath(partialSkeletonIndex); if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; @@ -288,8 +295,7 @@ internal record ResolveContext(GlobalResolveContext Global, PointerSkeletonParameterResourceHandle == null) return null; - if (!Utf8GamePath.FromByteString(ResolveSkpPath(CharacterBase, partialSkeletonIndex), out var path)) - return null; + var path = ResolveSkeletonParameterPath(partialSkeletonIndex); if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonParameterResourceHandle), out var cached)) return cached; @@ -305,43 +311,6 @@ internal record ResolveContext(GlobalResolveContext Global, Pointer 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{Global.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); diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 7c58d6a8..5e96d8bf 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.String.Classes; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; @@ -64,25 +65,13 @@ public class ResourceTree CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; - var genericContext = globalContext.CreateContext(model, 0xFFFFFFFFu, EquipSlot.Unknown, default); - - var eid = (ResourceHandle*)model->EID; - var eidNode = genericContext.CreateNodeFromEid(eid); - if (eidNode != null) - { - if (globalContext.WithUiData) - eidNode.FallbackName = "EID"; - Nodes.Add(eidNode); - } + var genericContext = globalContext.CreateContext(model); for (var i = 0; i < model->SlotCount; ++i) { - var slotContext = globalContext.CreateContext( - model, - (uint)i, - 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 = slotContext.CreateNodeFromImc(imc); @@ -94,7 +83,7 @@ public class ResourceTree } var mdl = model->Models[i]; - var mdlNode = slotContext.CreateNodeFromRenderModel(mdl); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty); if (mdlNode != null) { if (globalContext.WithUiData) @@ -103,77 +92,68 @@ public class ResourceTree } } - AddSkeleton(Nodes, genericContext, model->Skeleton); + AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton); - AddSubObjects(globalContext, model); + AddWeapons(globalContext, model); if (human != null) AddHumanResources(globalContext, human); } - private unsafe void AddSubObjects(GlobalResolveContext globalContext, CharacterBase* model) + private unsafe void AddWeapons(GlobalResolveContext globalContext, CharacterBase* model) { - var subObjectIndex = 0; - var weaponIndex = 0; - var subObjectNodes = new List(); + var weaponIndex = 0; + var weaponNodes = new List(); foreach (var baseSubObject in model->DrawObject.Object.ChildObjects) { if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) continue; var subObject = (CharacterBase*)baseSubObject; - var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null; - var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc."; + 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 = weapon != null ? (weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand) : EquipSlot.Unknown; - var equipment = weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default; + 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); - - var eid = (ResourceHandle*)subObject->EID; - var eidNode = genericContext.CreateNodeFromEid(eid); - if (eidNode != null) - { - if (globalContext.WithUiData) - eidNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, EID"; - Nodes.Add(eidNode); - } + var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); for (var i = 0; i < subObject->SlotCount; ++i) { - var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment); + var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType); var imc = (ResourceHandle*)subObject->IMCArray[i]; var imcNode = slotContext.CreateNodeFromImc(imc); if (imcNode != null) { if (globalContext.WithUiData) - imcNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}"; - subObjectNodes.Add(imcNode); + imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}"; + weaponNodes.Add(imcNode); } var mdl = subObject->Models[i]; - var mdlNode = slotContext.CreateNodeFromRenderModel(mdl); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty); if (mdlNode != null) { if (globalContext.WithUiData) - mdlNode.FallbackName = $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}"; - subObjectNodes.Add(mdlNode); + mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}"; + weaponNodes.Add(mdlNode); } } - AddSkeleton(subObjectNodes, genericContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, "); + AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, "); - ++subObjectIndex; - if (weapon != null) - ++weaponIndex; + ++weaponIndex; } - Nodes.InsertRange(0, subObjectNodes); + Nodes.InsertRange(0, weaponNodes); } private unsafe void AddHumanResources(GlobalResolveContext globalContext, Human* human) { - var genericContext = globalContext.CreateContext(&human->CharacterBase, 0xFFFFFFFFu, EquipSlot.Unknown, default); + var genericContext = globalContext.CreateContext(&human->CharacterBase); var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F); var decalPath = decalId != 0 @@ -208,8 +188,16 @@ 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; 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/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/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 94bc2428..e3c31a42 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 { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index df470d63..d7daaf70 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -76,6 +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. _collectionManager.Caches.CreateNecessaryCaches(); using (var t = _services.GetRequiredService().Measure(StartTimeType.PathResolver)) { diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 6a522ca2..2c4f385d 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -90,7 +90,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddConfiguration(this IServiceCollection services) => services.AddTransient() From b2bf6eb0f7615f54ac5f19b76b35555918987c76 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 13 Nov 2023 07:44:48 +0100 Subject: [PATCH 10/15] ResourceTree: Handle weapon MTRL special cases --- .../ResolveContext.PathResolution.cs | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index bcc957df..d7d80c21 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -13,7 +13,6 @@ namespace Penumbra.Interop.ResourceTree; internal partial record ResolveContext { - private Utf8GamePath ResolveModelPath() { // Correctness: @@ -83,27 +82,57 @@ internal partial record ResolveContext { ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), - ModelType.Weapon => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), + ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imcPath, mtrlFileName), _ => ResolveMaterialPathNative(mtrlFileName), }; } private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) { - var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - var modelPathSpan = modelPath.Path.Span; - var baseDirectory = modelPathSpan[..modelPathSpan.IndexOf("/model/"u8)]; - - var variant = ResolveMaterialVariant(imcPath); + var variant = ResolveMaterialVariant(imcPath); + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; - baseDirectory.CopyTo(pathBuffer); - "/material/v"u8.CopyTo(pathBuffer[baseDirectory.Length..]); - WriteZeroPaddedNumber(pathBuffer.Slice(baseDirectory.Length + 11, 4), variant); - pathBuffer[baseDirectory.Length + 15] = (byte)'/'; - fileName.CopyTo(pathBuffer[(baseDirectory.Length + 16)..]); + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); - return Utf8GamePath.FromSpan(pathBuffer[..(baseDirectory.Length + 16 + fileName.Length)], out var path) ? path.Clone() : Utf8GamePath.Empty; + return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveWeaponMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, 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(imcPath); + 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, imcPath, mtrlFileName); } private byte ResolveMaterialVariant(Utf8GamePath imcPath) @@ -119,6 +148,23 @@ internal partial record ResolveContext 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;) From 60551c87393e4a2b988852688040b7a4ba36f441 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 14 Nov 2023 20:38:21 +0100 Subject: [PATCH 11/15] ResourceTree: Are we fast yet? --- Penumbra/Collections/Cache/MetaCache.cs | 5 --- .../ResolveContext.PathResolution.cs | 31 +++++++++++-------- .../Interop/ResourceTree/ResolveContext.cs | 8 ++--- Penumbra/Interop/ResourceTree/ResourceTree.cs | 4 +-- Penumbra/Meta/Files/ImcFile.cs | 10 +++++- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 0da11022..d5acf249 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -187,11 +187,6 @@ public class MetaCache : IDisposable, IEnumerable _imcCache.GetImcFile(path, out file); - public ImcEntry GetImcEntry(Utf8GamePath path, EquipSlot slot, Variant variantIdx, out bool exists) - => GetImcFile(path, out var file) - ? file.GetEntry(Meta.Files.ImcFile.PartIndex(slot), variantIdx, out exists) - : Meta.Files.ImcFile.GetDefault(_manager, path, slot, variantIdx, out exists); - internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, SetId setId) { var eqdpFile = _eqdpCache.EqdpFile(race, accessory); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index d7d80c21..f4081de1 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -1,8 +1,10 @@ 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; @@ -74,22 +76,22 @@ internal partial record ResolveContext return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } - private unsafe Utf8GamePath ResolveMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + private unsafe Utf8GamePath ResolveMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { // Safety: // Resolving a material path through the game's code can dereference null pointers for equipment materials. return ModelType switch { - ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), - ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName), - ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imcPath, mtrlFileName), + 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), _ => ResolveMaterialPathNative(mtrlFileName), }; } - private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { - var variant = ResolveMaterialVariant(imcPath); + var variant = ResolveMaterialVariant(imc); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; @@ -98,7 +100,7 @@ internal partial record ResolveContext return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; } - private unsafe Utf8GamePath ResolveWeaponMaterialPath(Utf8GamePath modelPath, Utf8GamePath imcPath, byte* mtrlFileName) + 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 @@ -111,7 +113,7 @@ internal partial record ResolveContext var setIdLow = Equipment.Set.Id % 100; if (setIdLow > 50) { - var variant = ResolveMaterialVariant(imcPath); + var variant = ResolveMaterialVariant(imc); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); var mirroredSetId = (ushort)(Equipment.Set.Id - 50); @@ -132,16 +134,19 @@ internal partial record ResolveContext } } - return ResolveEquipmentMaterialPath(modelPath, imcPath, mtrlFileName); + return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); } - private byte ResolveMaterialVariant(Utf8GamePath imcPath) + private unsafe byte ResolveMaterialVariant(ResourceHandle* imc) { - var metaCache = Global.Collection.MetaCache; - if (metaCache == null) + 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 Equipment.Variant.Id; + } - var entry = metaCache.GetImcEntry(imcPath, Slot, Equipment.Variant, out var exists); + var entry = ImcFile.GetEntry(imcFileData, Slot, Equipment.Variant, out var exists); if (!exists) return Equipment.Variant.Id; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index f34a6ae2..73abcb4d 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -148,7 +148,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, PointerTexture, &tex->ResourceHandle, path); } - public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, Utf8GamePath imcPath) + public unsafe ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc) { if (mdl == null || mdl->ModelResourceHandle == null) return null; @@ -169,7 +169,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, Pointer= 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; @@ -367,7 +367,7 @@ internal partial record ResolveContext(GlobalResolveContext Global, PointerModels[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); if (mdlNode != null) { if (globalContext.WithUiData) @@ -135,7 +135,7 @@ public class ResourceTree } var mdl = subObject->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imcNode?.GamePath ?? Utf8GamePath.Empty); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); if (mdlNode != null) { if (globalContext.WithUiData) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index e3c31a42..68d3f5b3 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -168,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; From cb43fed9d3785674ef016f4f7b3cbf968ef89dd0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 14 Nov 2023 21:13:54 +0100 Subject: [PATCH 12/15] ResourceTree: Handle monster MTRL --- .../ResolveContext.PathResolution.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index f4081de1..1c9dfaa1 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -78,20 +78,21 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { - // Safety: - // Resolving a material path through the game's code can dereference null pointers for equipment materials. + // 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); + var variant = ResolveMaterialVariant(imc, Equipment.Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; @@ -113,7 +114,7 @@ internal partial record ResolveContext var setIdLow = Equipment.Set.Id % 100; if (setIdLow > 50) { - var variant = ResolveMaterialVariant(imc); + var variant = ResolveMaterialVariant(imc, Equipment.Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); var mirroredSetId = (ushort)(Equipment.Set.Id - 50); @@ -137,18 +138,30 @@ internal partial record ResolveContext return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); } - private unsafe byte ResolveMaterialVariant(ResourceHandle* imc) + 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 Equipment.Variant.Id; + return variant.Id; } - var entry = ImcFile.GetEntry(imcFileData, Slot, Equipment.Variant, out var exists); + var entry = ImcFile.GetEntry(imcFileData, Slot, variant, out var exists); if (!exists) - return Equipment.Variant.Id; + return variant.Id; return entry.MaterialId; } From acfd5d24848988ea0232bee49c49b9bec7fb9cc0 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 15 Nov 2023 20:04:30 +0100 Subject: [PATCH 13/15] Update Penumbra.GameData Also remove a check that, if it was still valid, would always be false with the new changes. --- Penumbra.GameData | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 20e8002b..a807e426 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 20e8002bfe701e54b05721c3b7b80c495a692adc +Subproject commit a807e426eed5b26a5d1043d5c47c98b28c93982e 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)) From 63ca0445867656980b8280282ac1cb0f14fa61fe Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 16 Nov 2023 05:48:30 +0100 Subject: [PATCH 14/15] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2e3237c9..c5c3b027 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2e3237c9018f4531f8ecdc1783b4f00e5eba7f34 +Subproject commit c5c3b0272ee47462c20b692787f9748571eb01dc From 8caba8c339375849e78a6b406a97379826ab6d84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Nov 2023 21:16:38 +0100 Subject: [PATCH 15/15] Update GameData again. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c5c3b027..ffdb966f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c5c3b0272ee47462c20b692787f9748571eb01dc +Subproject commit ffdb966fec5a657893289e655c641ceb3af1d59f