From 447e748ed7eb21eb81e1e48b61fcb122fe2bad40 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Jan 2024 13:23:37 +0100 Subject: [PATCH] start --- Glamourer/GameData/CustomizeParameterValue.cs | 4 +- Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 18 ++ .../Interop/Material/DirectXTextureHelper.cs | 116 ++++++++ Glamourer/Interop/Material/MaterialService.cs | 130 +++++++++ .../Interop/Material/MaterialValueIndex.cs | 266 ++++++++++++++++++ .../Interop/Material/MaterialValueManager.cs | 162 +++++++++++ Glamourer/Interop/Material/PrepareColorSet.cs | 162 +++++++++++ .../Interop/Material/SafeTextureHandle.cs | 49 ++++ Glamourer/Services/ServiceManager.cs | 3 +- 9 files changed, 906 insertions(+), 4 deletions(-) create mode 100644 Glamourer/Interop/Material/DirectXTextureHelper.cs create mode 100644 Glamourer/Interop/Material/MaterialService.cs create mode 100644 Glamourer/Interop/Material/MaterialValueIndex.cs create mode 100644 Glamourer/Interop/Material/MaterialValueManager.cs create mode 100644 Glamourer/Interop/Material/PrepareColorSet.cs create mode 100644 Glamourer/Interop/Material/SafeTextureHandle.cs diff --git a/Glamourer/GameData/CustomizeParameterValue.cs b/Glamourer/GameData/CustomizeParameterValue.cs index e1e0943..0e22d18 100644 --- a/Glamourer/GameData/CustomizeParameterValue.cs +++ b/Glamourer/GameData/CustomizeParameterValue.cs @@ -1,6 +1,4 @@ -using Newtonsoft.Json; - -namespace Glamourer.GameData; +namespace Glamourer.GameData; public readonly struct CustomizeParameterValue { diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index bef2ea5..68eb89e 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -8,6 +8,7 @@ using Glamourer.Designs; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Interop; +using Glamourer.Interop.Material; using Glamourer.Interop.Structs; using Glamourer.State; using ImGuiNET; @@ -97,6 +98,12 @@ public class ActorPanel( return (_selector.IncognitoMode ? _identifier.Incognito(null) : _identifier.ToString(), Actor.Null); } + private Vector3 _test; + private int _rowId; + private MaterialValueIndex.ColorTableIndex _index; + private int _materialId; + private int _slotId; + private unsafe void DrawPanel() { using var child = ImRaii.Child("##Panel", -Vector2.One, true); @@ -114,6 +121,17 @@ public class ActorPanel( RevertButtons(); + ImGui.InputInt("Row", ref _rowId); + ImGui.InputInt("Material", ref _materialId); + ImGui.InputInt("Slot", ref _slotId); + ImGuiUtil.GenericEnumCombo("Value", 300, _index, out _index); + + var index = new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Human, (byte) _slotId, (byte) _materialId, (byte)_rowId, _index); + index.TryGetValue(_actor, out _test); + if (ImGui.ColorPicker3("TestPicker", ref _test) && _actor.Valid) + MaterialService.Test(_actor, index, _test); + + using var disabled = ImRaii.Disabled(transformationId != 0); if (_state.ModelData.IsHuman) DrawHumanPanel(); diff --git a/Glamourer/Interop/Material/DirectXTextureHelper.cs b/Glamourer/Interop/Material/DirectXTextureHelper.cs new file mode 100644 index 0000000..9932abc --- /dev/null +++ b/Glamourer/Interop/Material/DirectXTextureHelper.cs @@ -0,0 +1,116 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using Penumbra.GameData.Files; +using Penumbra.String.Functions; +using SharpGen.Runtime; +using Vortice.Direct3D11; +using Vortice.DXGI; +using MapFlags = Vortice.Direct3D11.MapFlags; + +namespace Glamourer.Interop.Material; + +public static unsafe class DirectXTextureHelper +{ + /// Try to turn a color table GPU-loaded texture (R16G16B16A16Float, 4 Width, 16 Height) into an actual color table. + /// A pointer to the internal texture struct containing the GPU handle. + /// The returned color table. + /// Whether the table could be fetched. + public static bool TryGetColorTable(Texture* texture, out MtrlFile.ColorTable table) + { + if (texture == null) + { + table = default; + return false; + } + + try + { + // Create direct x resource and ensure that it is kept alive. + using var tex = new ID3D11Texture2D1((nint)texture->D3D11Texture2D); + tex.AddRef(); + + table = GetResourceData(tex, CreateStagedClone, GetTextureData); + return true; + } + catch + { + return false; + } + } + + /// Create a staging clone of the existing texture handle for stability reasons. + private static ID3D11Texture2D1 CreateStagedClone(ID3D11Texture2D1 resource) + { + var desc = resource.Description1 with + { + Usage = ResourceUsage.Staging, + BindFlags = 0, + CPUAccessFlags = CpuAccessFlags.Read, + MiscFlags = 0, + }; + + return resource.Device.As().CreateTexture2D1(desc); + } + + /// Turn a mapped texture into a color table. + private static MtrlFile.ColorTable GetTextureData(ID3D11Texture2D1 resource, MappedSubresource map) + { + var desc = resource.Description1; + + if (desc.Format is not Format.R16G16B16A16_Float + || desc.Width != MaterialService.TextureWidth + || desc.Height != MaterialService.TextureHeight + || map.DepthPitch != map.RowPitch * desc.Height) + throw new InvalidDataException("The texture was not a valid color table texture."); + + return ReadTexture(map.DataPointer, map.DepthPitch, desc.Height, map.RowPitch); + } + + /// Transform the GPU data into the color table. + /// The pointer to the raw texture data. + /// The size of the raw texture data. + /// The height of the texture. (Needs to be 16). + /// The stride in the texture data. + /// + private static MtrlFile.ColorTable ReadTexture(nint data, int length, int height, int pitch) + { + // Check that the data has sufficient dimension and size. + var expectedSize = sizeof(Half) * MaterialService.TextureWidth * height * 4; + if (length < expectedSize || sizeof(MtrlFile.ColorTable) != expectedSize || height != MaterialService.TextureHeight) + return default; + + var ret = new MtrlFile.ColorTable(); + var target = (byte*)&ret; + // If the stride is the same as in the table, just copy. + if (pitch == MaterialService.TextureWidth) + MemoryUtility.MemCpyUnchecked(target, (void*)data, length); + // Otherwise, adapt the stride. + else + + for (var y = 0; y < height; ++y) + { + MemoryUtility.MemCpyUnchecked(target + y * MaterialService.TextureWidth * sizeof(Half) * 4, (byte*)data + y * pitch, + MaterialService.TextureWidth * sizeof(Half) * 4); + } + + return ret; + } + + /// Get resources of a texture. + private static TRet GetResourceData(T res, Func cloneResource, Func getData) + where T : ID3D11Resource + { + using var stagingRes = cloneResource(res); + + res.Device.ImmediateContext.CopyResource(stagingRes, res); + stagingRes.Device.ImmediateContext.Map(stagingRes, 0, MapMode.Read, MapFlags.None, out var mapInfo).CheckError(); + + try + { + return getData(stagingRes, mapInfo); + } + finally + { + stagingRes.Device.ImmediateContext.Unmap(stagingRes, 0); + } + } +} diff --git a/Glamourer/Interop/Material/MaterialService.cs b/Glamourer/Interop/Material/MaterialService.cs new file mode 100644 index 0000000..91635c5 --- /dev/null +++ b/Glamourer/Interop/Material/MaterialService.cs @@ -0,0 +1,130 @@ +using Dalamud.Interface.Utility.Raii; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Glamourer.Interop.Structs; +using ImGuiNET; +using Lumina.Data.Files; +using OtterGui; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using static Penumbra.GameData.Files.MtrlFile; +using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; + +namespace Glamourer.Interop.Material; + + +public class MaterialServiceDrawer(ActorManager actors) : IService +{ + private ActorIdentifier _openIdentifier; + private uint _openSlotIndex; + + public unsafe void PopupButton(Actor actor, EquipSlot slot) + { + var slotIndex = slot.ToIndex(); + + var identifier = actor.GetIdentifier(actors); + var buttonActive = actor.Valid + && identifier.IsValid + && actor.Model.IsCharacterBase + && slotIndex < actor.Model.AsCharacterBase->SlotCount + && (actor.Model.AsCharacterBase->HasModelInSlotLoaded & (1 << (int)slotIndex)) != 0; + using var id = ImRaii.PushId((int)slot); + if (ImGuiUtil.DrawDisabledButton("Advanced", Vector2.Zero, "Open advanced window.", !buttonActive)) + { + _openIdentifier = identifier; + _openSlotIndex = slotIndex; + ImGui.OpenPopup($"Popup{slot}"); + } + } +} + +public static unsafe class MaterialService +{ + public const int TextureWidth = 4; + public const int TextureHeight = ColorTable.NumRows; + public const int MaterialsPerModel = 4; + + /// Generate a color table the way the game does inside the original texture, and release the original. + /// The original texture that will be replaced with a new one. + /// The input color table. + /// Success or failure. + public static bool GenerateColorTable(Texture** original, in ColorTable colorTable) + { + if (original == null) + return false; + + var textureSize = stackalloc int[2]; + textureSize[0] = TextureWidth; + textureSize[1] = TextureHeight; + + using var texture = new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, (uint)TexFile.TextureFormat.R16G16B16A16F, + (uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7), false); + if (texture.IsInvalid) + return false; + + fixed(ColorTable* ptr = &colorTable) + { + if (!texture.Texture->InitializeContents(ptr)) + return false; + } + + texture.Exchange(ref *(nint*)original); + return true; + } + + + /// Obtain a pointer to the models pointer to a specific color table texture. + /// + /// + /// + /// + public static Texture** GetColorTableTexture(Model model, int modelSlot, byte materialSlot) + { + if (!model.IsCharacterBase) + return null; + + var index = modelSlot * MaterialsPerModel + materialSlot; + if (index < 0 || index >= model.AsCharacterBase->ColorTableTexturesSpan.Length) + return null; + + var texture = (Texture**)Unsafe.AsPointer(ref model.AsCharacterBase->ColorTableTexturesSpan[index]); + return texture; + } + + /// Obtain a pointer to the color table of a certain material from a model. + /// The draw object. + /// The model slot. + /// The material slot in the model. + /// A pointer to the color table or null. + public static ColorTable* GetMaterialColorTable(Model model, int modelSlot, byte materialSlot) + { + if (!model.IsCharacterBase) + return null; + + var index = modelSlot * MaterialsPerModel + materialSlot; + if (index < 0 || index >= model.AsCharacterBase->MaterialsSpan.Length) + return null; + + var material = (MaterialResourceHandle*)model.AsCharacterBase->MaterialsSpan[index].Value; + if (material == null || material->ColorTable == null) + return null; + + return (ColorTable*)material->ColorTable; + } + + public static void Test(Actor actor, MaterialValueIndex index, Vector3 value) + { + if (!index.TryGetColorTable(actor, out var table)) + return; + + ref var row = ref table[index.RowIndex]; + if (!index.DataIndex.SetValue(ref row, value)) + return; + + var texture = GetColorTableTexture(index.TryGetModel(actor, out var model) ? model : Model.Null, index.SlotIndex, + index.MaterialIndex); + if (texture != null) + GenerateColorTable(texture, table); + } +} diff --git a/Glamourer/Interop/Material/MaterialValueIndex.cs b/Glamourer/Interop/Material/MaterialValueIndex.cs new file mode 100644 index 0000000..4cbc116 --- /dev/null +++ b/Glamourer/Interop/Material/MaterialValueIndex.cs @@ -0,0 +1,266 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.Interop; +using Glamourer.Interop.Structs; +using Newtonsoft.Json; +using Penumbra.GameData.Files; + +namespace Glamourer.Interop.Material; + +[JsonConverter(typeof(Converter))] +public readonly record struct MaterialValueIndex( + MaterialValueIndex.DrawObjectType DrawObject, + byte SlotIndex, + byte MaterialIndex, + byte RowIndex, + MaterialValueIndex.ColorTableIndex DataIndex) +{ + public uint Key + => ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex, DataIndex); + + public bool Valid + => Validate(DrawObject) && ValidateSlot(SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex) && Validate(DataIndex); + + public static bool FromKey(uint key, out MaterialValueIndex index) + { + index = new MaterialValueIndex(key); + return index.Valid; + } + + public unsafe bool TryGetModel(Actor actor, out Model model) + { + if (!actor.Valid) + { + model = Model.Null; + return false; + } + + model = DrawObject switch + { + DrawObjectType.Human => actor.Model, + DrawObjectType.Mainhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponDataSpan[0].DrawObject : Model.Null, + DrawObjectType.Offhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject : Model.Null, + _ => Model.Null, + }; + return model.IsCharacterBase; + } + + public unsafe bool TryGetTextures(Actor actor, out ReadOnlySpan> textures) + { + if (!TryGetModel(actor, out var model) + || SlotIndex >= model.AsCharacterBase->SlotCount + || model.AsCharacterBase->ColorTableTexturesSpan.Length < (SlotIndex + 1) * MaterialService.MaterialsPerModel) + { + textures = []; + return false; + } + + textures = model.AsCharacterBase->ColorTableTexturesSpan.Slice(SlotIndex * MaterialService.MaterialsPerModel, + MaterialService.MaterialsPerModel); + return true; + } + + public unsafe bool TryGetTexture(Actor actor, out Texture* texture) + { + if (!TryGetTextures(actor, out var textures) || MaterialIndex >= MaterialService.MaterialsPerModel) + { + texture = null; + return false; + } + + texture = textures[MaterialIndex].Value; + return texture != null; + } + + public unsafe bool TryGetColorTable(Actor actor, out MtrlFile.ColorTable table) + { + if (TryGetTexture(actor, out var texture)) + return DirectXTextureHelper.TryGetColorTable(texture, out table); + + table = default; + return false; + } + + public unsafe bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row) + { + if (!TryGetColorTable(actor, out var table)) + { + row = default; + return false; + } + + row = table[RowIndex]; + return true; + } + + public unsafe bool TryGetValue(Actor actor, out Vector3 value) + { + if (!TryGetColorRow(actor, out var row)) + { + value = Vector3.Zero; + return false; + } + + value = DataIndex switch + { + ColorTableIndex.Diffuse => row.Diffuse, + ColorTableIndex.Specular => row.Specular, + ColorTableIndex.SpecularStrength => new Vector3(row.SpecularStrength, 0, 0), + ColorTableIndex.Emissive => row.Emissive, + ColorTableIndex.GlossStrength => new Vector3(row.GlossStrength, 0, 0), + ColorTableIndex.TileSet => new Vector3(row.TileSet), + ColorTableIndex.MaterialRepeat => new Vector3(row.MaterialRepeat, 0), + ColorTableIndex.MaterialSkew => new Vector3(row.MaterialSkew, 0), + _ => new Vector3(float.NaN), + }; + return !float.IsNaN(value.X); + } + + public static MaterialValueIndex FromKey(uint key) + => new(key); + + public static MaterialValueIndex Min(DrawObjectType drawObject = 0, byte slotIndex = 0, byte materialIndex = 0, byte rowIndex = 0, + ColorTableIndex dataIndex = 0) + => new(drawObject, slotIndex, materialIndex, rowIndex, dataIndex); + + public static MaterialValueIndex Max(DrawObjectType drawObject = (DrawObjectType)byte.MaxValue, byte slotIndex = byte.MaxValue, + byte materialIndex = byte.MaxValue, byte rowIndex = byte.MaxValue, + ColorTableIndex dataIndex = (ColorTableIndex)byte.MaxValue) + => new(drawObject, slotIndex, materialIndex, rowIndex, dataIndex); + + public enum DrawObjectType : byte + { + Human, + Mainhand, + Offhand, + }; + + public enum ColorTableIndex : byte + { + Diffuse, + Specular, + SpecularStrength, + Emissive, + GlossStrength, + TileSet, + MaterialRepeat, + MaterialSkew, + } + + public static bool Validate(DrawObjectType type) + => Enum.IsDefined(type); + + public static bool ValidateSlot(byte slotIndex) + => slotIndex < 10; + + public static bool ValidateMaterial(byte materialIndex) + => materialIndex < MaterialService.MaterialsPerModel; + + public static bool ValidateRow(byte rowIndex) + => rowIndex < MtrlFile.ColorTable.NumRows; + + public static bool Validate(ColorTableIndex dataIndex) + => Enum.IsDefined(dataIndex); + + private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex, ColorTableIndex index) + { + var result = (uint)index & 0xFF; + result |= (uint)(rowIndex & 0xFF) << 8; + result |= (uint)(materialIndex & 0xF) << 16; + result |= (uint)(slotIndex & 0xFF) << 20; + result |= (uint)((byte)type & 0xF) << 28; + return result; + } + + private MaterialValueIndex(uint key) + : this((DrawObjectType)((key >> 28) & 0xF), (byte)(key >> 20), (byte)((key >> 16) & 0xF), (byte)(key >> 8), + (ColorTableIndex)(key & 0xFF)) + { } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MaterialValueIndex value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Key); + + public override MaterialValueIndex ReadJson(JsonReader reader, Type objectType, MaterialValueIndex existingValue, bool hasExistingValue, + JsonSerializer serializer) + => FromKey(serializer.Deserialize(reader), out var value) ? value : throw new Exception($"Invalid material key {value.Key}."); + } +} + +public static class MaterialExtensions +{ + public static bool TryGetValue(this MaterialValueIndex.ColorTableIndex index, in MtrlFile.ColorTable.Row row, out Vector3 value) + { + value = index switch + { + MaterialValueIndex.ColorTableIndex.Diffuse => row.Diffuse, + MaterialValueIndex.ColorTableIndex.Specular => row.Specular, + MaterialValueIndex.ColorTableIndex.SpecularStrength => new Vector3(row.SpecularStrength, 0, 0), + MaterialValueIndex.ColorTableIndex.Emissive => row.Emissive, + MaterialValueIndex.ColorTableIndex.GlossStrength => new Vector3(row.GlossStrength, 0, 0), + MaterialValueIndex.ColorTableIndex.TileSet => new Vector3(row.TileSet), + MaterialValueIndex.ColorTableIndex.MaterialRepeat => new Vector3(row.MaterialRepeat, 0), + MaterialValueIndex.ColorTableIndex.MaterialSkew => new Vector3(row.MaterialSkew, 0), + _ => new Vector3(float.NaN), + }; + return !float.IsNaN(value.X); + } + + public static bool SetValue(this MaterialValueIndex.ColorTableIndex index, ref MtrlFile.ColorTable.Row row, in Vector3 value) + { + switch (index) + { + case MaterialValueIndex.ColorTableIndex.Diffuse: + if (value == row.Diffuse) + return false; + + row.Diffuse = value; + return true; + + case MaterialValueIndex.ColorTableIndex.Specular: + if (value == row.Specular) + return false; + + row.Specular = value; + return true; + case MaterialValueIndex.ColorTableIndex.SpecularStrength: + if (value.X == row.SpecularStrength) + return false; + + row.SpecularStrength = value.X; + return true; + case MaterialValueIndex.ColorTableIndex.Emissive: + if (value == row.Emissive) + return false; + + row.Emissive = value; + return true; + case MaterialValueIndex.ColorTableIndex.GlossStrength: + if (value.X == row.GlossStrength) + return false; + + row.GlossStrength = value.X; + return true; + case MaterialValueIndex.ColorTableIndex.TileSet: + var @ushort = (ushort)(value.X + 0.5f); + if (@ushort == row.TileSet) + return false; + + row.TileSet = @ushort; + return true; + case MaterialValueIndex.ColorTableIndex.MaterialRepeat: + if (value.X == row.MaterialRepeat.X && value.Y == row.MaterialRepeat.Y) + return false; + + row.MaterialRepeat = new Vector2(value.X, value.Y); + return true; + case MaterialValueIndex.ColorTableIndex.MaterialSkew: + if (value.X == row.MaterialSkew.X && value.Y == row.MaterialSkew.Y) + return false; + + row.MaterialSkew = new Vector2(value.X, value.Y); + return true; + default: return false; + } + } +} diff --git a/Glamourer/Interop/Material/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs new file mode 100644 index 0000000..257dfc8 --- /dev/null +++ b/Glamourer/Interop/Material/MaterialValueManager.cs @@ -0,0 +1,162 @@ +namespace Glamourer.Interop.Material; + +public readonly struct MaterialValueManager +{ + private readonly List<(uint Key, Vector3 Value)> _values = []; + + public MaterialValueManager() + { } + + public bool TryGetValue(MaterialValueIndex index, out Vector3 value) + { + if (_values.Count == 0) + { + value = Vector3.Zero; + return false; + } + + var idx = Search(index.Key); + if (idx >= 0) + { + value = _values[idx].Value; + return true; + } + + value = Vector3.Zero; + return false; + } + + public bool TryAddValue(MaterialValueIndex index, in Vector3 value) + { + var key = index.Key; + var idx = Search(key); + if (idx >= 0) + return false; + + _values.Insert(~idx, (key, value)); + return true; + } + + public bool RemoveValue(MaterialValueIndex index) + { + if (_values.Count == 0) + return false; + + var idx = Search(index.Key); + if (idx < 0) + return false; + + _values.RemoveAt(idx); + return true; + } + + public void AddOrUpdateValue(MaterialValueIndex index, in Vector3 value) + { + var key = index.Key; + var idx = Search(key); + if (idx < 0) + _values.Insert(~idx, (key, value)); + else + _values[idx] = (key, value); + } + + public bool UpdateValue(MaterialValueIndex index, in Vector3 value, out Vector3 oldValue) + { + if (_values.Count == 0) + { + oldValue = Vector3.Zero; + return false; + } + + var key = index.Key; + var idx = Search(key); + if (idx < 0) + { + oldValue = Vector3.Zero; + return false; + } + + oldValue = _values[idx].Value; + _values[idx] = (key, value); + return true; + } + + public int RemoveValues(MaterialValueIndex min, MaterialValueIndex max) + { + var (minIdx, maxIdx) = GetMinMax(CollectionsMarshal.AsSpan(_values), min.Key, max.Key); + if (minIdx < 0) + return 0; + + var count = maxIdx - minIdx; + _values.RemoveRange(minIdx, count); + return count; + } + + public ReadOnlySpan<(uint key, Vector3 Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) + => Filter(CollectionsMarshal.AsSpan(_values), min, max); + + public static ReadOnlySpan<(uint Key, Vector3 Value)> Filter(ReadOnlySpan<(uint Key, Vector3 Value)> values, MaterialValueIndex min, + MaterialValueIndex max) + { + var (minIdx, maxIdx) = GetMinMax(values, min.Key, max.Key); + return minIdx < 0 ? [] : values[minIdx..(maxIdx - minIdx)]; + } + + /// Obtain the minimum index and maximum index for a minimum and maximum key. + private static (int MinIdx, int MaxIdx) GetMinMax(ReadOnlySpan<(uint Key, Vector3 Value)> values, uint minKey, uint maxKey) + { + // Find the minimum index by binary search. + var idx = values.BinarySearch((minKey, Vector3.Zero), Comparer.Instance); + var minIdx = idx; + + // If the key does not exist, check if it is an invalid range or set it correctly. + if (minIdx < 0) + { + minIdx = ~minIdx; + if (minIdx == values.Length || values[minIdx].Key > maxKey) + return (-1, -1); + + idx = minIdx; + } + else + { + // If it does exist, go upwards until the first key is reached that is actually smaller. + while (minIdx > 0 && values[minIdx - 1].Key >= minKey) + --minIdx; + } + + // Check if the range can be valid. + if (values[minIdx].Key < minKey || values[minIdx].Key > maxKey) + return (-1, -1); + + + // Do pretty much the same but in the other direction with the maximum key. + var maxIdx = values[idx..].BinarySearch((maxKey, Vector3.Zero), Comparer.Instance); + if (maxIdx < 0) + { + maxIdx = ~maxIdx; + return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1); + } + + while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey) + ++maxIdx; + + if (values[maxIdx].Key < minKey || values[maxIdx].Key > maxKey) + return (-1, -1); + + return (minIdx, maxIdx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private int Search(uint key) + => _values.BinarySearch((key, Vector3.Zero), Comparer.Instance); + + private class Comparer : IComparer<(uint Key, Vector3 Value)> + { + public static readonly Comparer Instance = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public int Compare((uint Key, Vector3 Value) x, (uint Key, Vector3 Value) y) + => x.Key.CompareTo(y.Key); + } +} diff --git a/Glamourer/Interop/Material/PrepareColorSet.cs b/Glamourer/Interop/Material/PrepareColorSet.cs new file mode 100644 index 0000000..8d44ee9 --- /dev/null +++ b/Glamourer/Interop/Material/PrepareColorSet.cs @@ -0,0 +1,162 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Glamourer.Interop.Penumbra; +using Glamourer.Interop.Structs; +using Glamourer.State; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.Material; + +public sealed unsafe class PrepareColorSet + : EventWrapperPtr12Ref34, IHookService +{ + public enum Priority + { + /// + MaterialManager = 0, + } + + public PrepareColorSet(HookManager hooks) + : base("Prepare Color Set ") + => _task = hooks.CreateHook(Name, "40 55 56 41 56 48 83 EC ?? 80 BA", Detour, true); + + private readonly Task> _task; + + public nint Address + => (nint)CharacterBase.MemberFunctionPointers.Destroy; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate Texture* Delegate(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId); + + private Texture* Detour(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId) + { + Glamourer.Log.Excessive($"[{Name}] Triggered with 0x{(nint)characterBase:X} 0x{(nint)material:X} {stainId.Id}."); + var ret = nint.Zero; + Invoke(characterBase, material, ref stainId, ref ret); + if (ret != nint.Zero) + return (Texture*)ret; + + return _task.Result.Original(characterBase, material, stainId); + } +} + +public sealed unsafe class MaterialManager : IRequiredService, IDisposable +{ + private readonly PrepareColorSet _event; + private readonly StateManager _stateManager; + private readonly PenumbraService _penumbra; + private readonly ActorManager _actors; + + private int _lastSlot; + + public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra) + { + _stateManager = stateManager; + _actors = actors; + _penumbra = penumbra; + _event = prepareColorSet; + + _event.Subscribe(OnPrepareColorSet, PrepareColorSet.Priority.MaterialManager); + } + + public void Dispose() + => _event.Unsubscribe(OnPrepareColorSet); + + private void OnPrepareColorSet(CharacterBase* characterBase, MaterialResourceHandle* material, ref StainId stain, ref nint ret) + { + var actor = _penumbra.GameObjectFromDrawObject(characterBase); + var validType = FindType(characterBase, actor, out var type); + var (slotId, materialId) = FindMaterial(characterBase, material); + Glamourer.Log.Information( + $" Triggered with 0x{(nint)characterBase:X} 0x{(nint)material:X} {stain.Id} --- Actor: 0x{actor.Address:X} Slot: {slotId} Material: {materialId} DrawObject: {type}."); + if (!validType + || slotId == byte.MaxValue + || !actor.Identifier(_actors, out var identifier) + || !_stateManager.TryGetValue(identifier, out var state)) + return; + + var min = MaterialValueIndex.Min(type, slotId, materialId); + var max = MaterialValueIndex.Max(type, slotId, materialId); + var manager = new MaterialValueManager(); + var values = manager.GetValues(min, max); + foreach (var (key, value) in values) + ; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private (byte SlotId, byte MaterialId) FindMaterial(CharacterBase* characterBase, MaterialResourceHandle* material) + { + for (var i = _lastSlot; i < characterBase->SlotCount; ++i) + { + var idx = MaterialService.MaterialsPerModel * i; + for (var j = 0; j < MaterialService.MaterialsPerModel; ++j) + { + var mat = (nint)characterBase->Materials[idx++]; + if (mat != (nint)material) + continue; + + _lastSlot = i; + return ((byte)i, (byte)j); + } + } + + for (var i = 0; i < _lastSlot; ++i) + { + var idx = MaterialService.MaterialsPerModel * i; + for (var j = 0; j < MaterialService.MaterialsPerModel; ++j) + { + var mat = (nint)characterBase->Materials[idx++]; + if (mat != (nint)material) + continue; + + _lastSlot = i; + return ((byte)i, (byte)j); + } + } + + return (byte.MaxValue, byte.MaxValue); + } + + private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type) + { + type = MaterialValueIndex.DrawObjectType.Human; + if (!actor.Valid) + return false; + + if (actor.Model.AsCharacterBase == characterBase) + return true; + + if (!actor.AsObject->IsCharacter()) + return false; + + if (actor.AsCharacter->DrawData.WeaponDataSpan[0].DrawObject == characterBase) + { + type = MaterialValueIndex.DrawObjectType.Mainhand; + return true; + } + + if (actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject == characterBase) + { + type = MaterialValueIndex.DrawObjectType.Offhand; + return true; + } + + return false; + } +} diff --git a/Glamourer/Interop/Material/SafeTextureHandle.cs b/Glamourer/Interop/Material/SafeTextureHandle.cs new file mode 100644 index 0000000..20e6f65 --- /dev/null +++ b/Glamourer/Interop/Material/SafeTextureHandle.cs @@ -0,0 +1,49 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; + +namespace Glamourer.Interop.Material; + +public unsafe class SafeTextureHandle : SafeHandle +{ + public Texture* Texture + => (Texture*)handle; + + public override bool IsInvalid + => handle == 0; + + public SafeTextureHandle(Texture* handle, bool incRef, bool ownsHandle = true) + : base(0, ownsHandle) + { + if (incRef && !ownsHandle) + throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported"); + + if (incRef && handle != null) + handle->IncRef(); + SetHandle((nint)handle); + } + + public void Exchange(ref nint ppTexture) + { + lock (this) + { + handle = Interlocked.Exchange(ref ppTexture, handle); + } + } + + public static SafeTextureHandle CreateInvalid() + => new(null, false); + + protected override bool ReleaseHandle() + { + nint handle; + lock (this) + { + handle = this.handle; + this.handle = 0; + } + + if (handle != 0) + ((Texture*)handle)->DecRef(); + + return true; + } +} \ No newline at end of file diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index d9d76b4..dd06e3a 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -20,6 +20,7 @@ using Glamourer.Unlocks; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Raii; using OtterGui.Services; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; @@ -47,7 +48,7 @@ public static class ServiceManagerA DalamudServices.AddServices(services, pi); services.AddIServices(typeof(EquipItem).Assembly); services.AddIServices(typeof(Glamourer).Assembly); - services.AddIServices(typeof(EquipFlag).Assembly); + services.AddIServices(typeof(ImRaii).Assembly); services.CreateProvider(); return services; }