diff --git a/Penumbra/DebugConfiguration.cs b/Penumbra/DebugConfiguration.cs index 76987df8..3f9e8207 100644 --- a/Penumbra/DebugConfiguration.cs +++ b/Penumbra/DebugConfiguration.cs @@ -2,5 +2,6 @@ namespace Penumbra; public class DebugConfiguration { - public static bool WriteImcBytesToLog = false; + public static bool WriteImcBytesToLog = false; + public static bool UseSkinMaterialProcessing = true; } diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 85fb1098..db39889e 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -6,6 +6,7 @@ using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Processing; using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler; namespace Penumbra.Interop.Hooks.Resources; @@ -159,7 +160,13 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName)); private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) - => ResolvePath(drawObject, _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); + { + var finalPathBuffer = _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex); + if (DebugConfiguration.UseSkinMaterialProcessing && finalPathBuffer != nint.Zero && finalPathBuffer == pathBuffer) + SkinMtrlPathEarlyProcessing.Process(new Span((void*)pathBuffer, (int)pathBufferSize), (CharacterBase*)drawObject, slotIndex); + + return ResolvePath(drawObject, finalPathBuffer); + } private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs new file mode 100644 index 00000000..4487eb7f --- /dev/null +++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs @@ -0,0 +1,64 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.Processing; + +public static unsafe class SkinMtrlPathEarlyProcessing +{ + public static void Process(Span path, CharacterBase* character, uint slotIndex) + { + var end = path.IndexOf(MaterialExtension()); + if (end < 0) + return; + + var suffixPos = path[..end].LastIndexOf((byte)'_'); + if (suffixPos < 0) + return; + + var handle = GetModelResourceHandle(character, slotIndex); + if (handle == null) + return; + + var skinSuffix = GetSkinSuffix(handle); + if (skinSuffix.IsEmpty || skinSuffix.Length > path.Length - suffixPos - 7) + return; + + ++suffixPos; + skinSuffix.CopyTo(path[suffixPos..]); + suffixPos += skinSuffix.Length; + MaterialExtension().CopyTo(path[suffixPos..]); + return; + + static ReadOnlySpan MaterialExtension() + => ".mtrl\0"u8; + } + + private static ModelResourceHandle* GetModelResourceHandle(CharacterBase* character, uint slotIndex) + { + if (character is null) + return null; + + if (character->TempSlotData is not null) + { + // TODO ClientStructs-ify + var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8); + if (handle != null) + return handle; + } + + var model = character->Models[slotIndex]; + return model is null ? null : model->ModelResourceHandle; + } + + private static ReadOnlySpan GetSkinSuffix(ModelResourceHandle* handle) + { + foreach (var (attribute, _) in handle->Attributes) + { + var attributeSpan = attribute.AsSpan(); + if (attributeSpan.Length > 12 && attributeSpan[..11].SequenceEqual("skin_suffix"u8) && attributeSpan[11] is (byte)'=' or (byte)'_') + return attributeSpan[12..]; + } + + return []; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs index 241c3a91..24a5f9c2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -131,7 +131,7 @@ public sealed unsafe class MaterialTemplatePickers : IUiService if (texture == null) continue; var handle = _textureArraySlicer.GetImGuiHandle(texture, sliceIndex); - if (handle == 0) + if (handle.IsNull) continue; var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; diff --git a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs index 97761091..087670c1 100644 --- a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs @@ -1,15 +1,16 @@ -using OtterGui.Text; - -namespace Penumbra.UI.Tabs.Debug; - +using OtterGui.Text; + +namespace Penumbra.UI.Tabs.Debug; + public static class DebugConfigurationDrawer { public static void Draw() { - using var id = ImUtf8.CollapsingHeaderId("Debug Logging Options"u8); + using var id = ImUtf8.CollapsingHeaderId("Debugging Options"u8); if (!id) return; - ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); + ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); + ImUtf8.Checkbox("Scan for Skin Material Attributes"u8, ref DebugConfiguration.UseSkinMaterialProcessing); } -} +}