From 447e748ed7eb21eb81e1e48b61fcb122fe2bad40 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Jan 2024 13:23:37 +0100 Subject: [PATCH 01/17] 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; } From beff7adec477841181f7eb130afd46d97382ea2e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Jan 2024 13:24:57 +0100 Subject: [PATCH 02/17] Add Vortice. --- Glamourer/Glamourer.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index 2ee5538..87954e9 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -89,6 +89,7 @@ + From 5e5ce4d234e161724223f6675d867ea26e5a0aee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Jan 2024 17:16:57 +0100 Subject: [PATCH 03/17] Add functions to compute current color table. --- Glamourer/Interop/Material/PrepareColorSet.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Glamourer/Interop/Material/PrepareColorSet.cs b/Glamourer/Interop/Material/PrepareColorSet.cs index 8d44ee9..137228e 100644 --- a/Glamourer/Interop/Material/PrepareColorSet.cs +++ b/Glamourer/Interop/Material/PrepareColorSet.cs @@ -1,4 +1,5 @@ -using Dalamud.Hooking; +using Dalamud.Game; +using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; @@ -8,6 +9,8 @@ using Glamourer.State; using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; using Penumbra.GameData.Structs; namespace Glamourer.Interop.Material; @@ -54,6 +57,31 @@ public sealed unsafe class PrepareColorSet return _task.Result.Original(characterBase, material, stainId); } + + public static MtrlFile.ColorTable GetColorTable(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId) + { + var table = new MtrlFile.ColorTable(); + characterBase->ReadStainingTemplate(material, stainId.Id, (Half*)(&table)); + return table; + } + + public static bool TryGetColorTable(Model model, byte slotIdx, out MtrlFile.ColorTable table) + { + var table2 = new MtrlFile.ColorTable(); + if (!model.IsCharacterBase || slotIdx < model.AsCharacterBase->SlotCount) + return false; + + var resource = (MaterialResourceHandle*)model.AsCharacterBase->Materials[slotIdx]; + var stain = model.AsCharacterBase->GetModelType() switch + { + CharacterBase.ModelType.Human => model.GetArmor(EquipSlotExtensions.ToEquipSlot(slotIdx)).Stain, + CharacterBase.ModelType.Weapon => (StainId)model.AsWeapon->ModelUnknown, + _ => (StainId)0, + }; + model.AsCharacterBase->ReadStainingTemplate(resource, stain.Id, (Half*)(&table2)); + table = table2; + return true; + } } public sealed unsafe class MaterialManager : IRequiredService, IDisposable @@ -85,6 +113,9 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable 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}."); + var table = PrepareColorSet.GetColorTable(characterBase, material, stain); + Glamourer.Log.Information($"{table[15].Diffuse}"); + if (!validType || slotId == byte.MaxValue || !actor.Identifier(_actors, out var identifier) From cb45221be27f22fbeee00fe6d8c2f200fc2549d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Jan 2024 00:32:48 +0100 Subject: [PATCH 04/17] Things are progressing at a satisfying rate. --- Glamourer/Gui/Materials/MaterialDrawer.cs | 178 ++++++++++++++++++ Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 28 ++- Glamourer/Interop/Material/MaterialService.cs | 69 ++----- .../Interop/Material/MaterialValueManager.cs | 53 ++++-- Glamourer/Interop/Material/PrepareColorSet.cs | 80 +++++--- Glamourer/State/ActorState.cs | 5 +- Glamourer/State/StateManager.cs | 2 + 7 files changed, 312 insertions(+), 103 deletions(-) create mode 100644 Glamourer/Gui/Materials/MaterialDrawer.cs diff --git a/Glamourer/Gui/Materials/MaterialDrawer.cs b/Glamourer/Gui/Materials/MaterialDrawer.cs new file mode 100644 index 0000000..1114911 --- /dev/null +++ b/Glamourer/Gui/Materials/MaterialDrawer.cs @@ -0,0 +1,178 @@ +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Interop.Material; +using Glamourer.Interop.Structs; +using ImGuiNET; +using OtterGui.Services; +using Penumbra.GameData.Files; + +namespace Glamourer.Gui.Materials; + +public unsafe class MaterialDrawer : IService +{ + private static readonly IReadOnlyList Types = + [ + MaterialValueIndex.DrawObjectType.Human, + MaterialValueIndex.DrawObjectType.Mainhand, + MaterialValueIndex.DrawObjectType.Offhand, + ]; + + public void DrawPanel(Actor actor) + { + if (!actor.IsCharacter) + return; + + foreach (var type in Types) + { + var index = new MaterialValueIndex(type, 0, 0, 0, 0); + if (index.TryGetModel(actor, out var model)) + DrawModelType(model, index); + } + } + + private void DrawModelType(Model model, MaterialValueIndex sourceIndex) + { + using var tree = ImRaii.TreeNode(sourceIndex.DrawObject.ToString()); + if (!tree) + return; + + var names = model.AsCharacterBase->GetModelType() is CharacterBase.ModelType.Human + ? SlotNamesHuman + : SlotNames; + for (byte i = 0; i < model.AsCharacterBase->SlotCount; ++i) + { + var index = sourceIndex with { SlotIndex = i }; + DrawSlot(model, names, index); + } + } + + private void DrawSlot(Model model, IReadOnlyList names, MaterialValueIndex sourceIndex) + { + using var tree = ImRaii.TreeNode(names[sourceIndex.SlotIndex]); + if (!tree) + return; + + for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i) + { + var index = sourceIndex with { MaterialIndex = i }; + var texture = model.AsCharacterBase->ColorTableTextures + index.SlotIndex * MaterialService.MaterialsPerModel + i; + if (*texture == null) + continue; + + if (!DirectXTextureHelper.TryGetColorTable(*texture, out var table)) + continue; + + DrawMaterial(ref table, texture, index); + } + } + + private void DrawMaterial(ref MtrlFile.ColorTable table, Texture** texture, MaterialValueIndex sourceIndex) + { + using var tree = ImRaii.TreeNode($"Material {sourceIndex.MaterialIndex + 1}"); + if (!tree) + return; + + for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i) + { + var index = sourceIndex with { RowIndex = i }; + ref var row = ref table[i]; + DrawRow(ref table, ref row, texture, index); + } + } + + private void DrawRow(ref MtrlFile.ColorTable table, ref MtrlFile.ColorTable.Row row, Texture** texture, MaterialValueIndex sourceIndex) + { + using var id = ImRaii.PushId(sourceIndex.RowIndex); + var diffuse = row.Diffuse; + var specular = row.Specular; + var emissive = row.Emissive; + var glossStrength = row.GlossStrength; + var specularStrength = row.SpecularStrength; + if (ImGui.ColorEdit3("Diffuse", ref diffuse, ImGuiColorEditFlags.NoInputs)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Diffuse }; + row.Diffuse = diffuse; + MaterialService.ReplaceColorTable(texture, table); + } + ImGui.SameLine(); + if (ImGui.ColorEdit3("Specular", ref specular, ImGuiColorEditFlags.NoInputs)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Specular }; + row.Specular = specular; + MaterialService.ReplaceColorTable(texture, table); + } + ImGui.SameLine(); + if (ImGui.ColorEdit3("Emissive", ref emissive, ImGuiColorEditFlags.NoInputs)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Emissive }; + row.Emissive = emissive; + MaterialService.ReplaceColorTable(texture, table); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + if (ImGui.DragFloat("Gloss", ref glossStrength, 0.1f)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.GlossStrength }; + row.GlossStrength = glossStrength; + MaterialService.ReplaceColorTable(texture, table); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + if (ImGui.DragFloat("Specular Strength", ref specularStrength, 0.1f)) + { + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.SpecularStrength }; + row.SpecularStrength = specularStrength; + MaterialService.ReplaceColorTable(texture, table); + } + } + + private static readonly IReadOnlyList SlotNames = + [ + "Slot 1", + "Slot 2", + "Slot 3", + "Slot 4", + "Slot 5", + "Slot 6", + "Slot 7", + "Slot 8", + "Slot 9", + "Slot 10", + "Slot 11", + "Slot 12", + "Slot 13", + "Slot 14", + "Slot 15", + "Slot 16", + "Slot 17", + "Slot 18", + "Slot 19", + "Slot 20", + ]; + + private static readonly IReadOnlyList SlotNamesHuman = + [ + "Head", + "Body", + "Hands", + "Legs", + "Feet", + "Earrings", + "Neck", + "Wrists", + "Right Finger", + "Left Finger", + "Slot 11", + "Slot 12", + "Slot 13", + "Slot 14", + "Slot 15", + "Slot 16", + "Slot 17", + "Slot 18", + "Slot 19", + "Slot 20", + ]; +} diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 68eb89e..6ec62f5 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -7,6 +7,7 @@ using Glamourer.Automation; using Glamourer.Designs; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; +using Glamourer.Gui.Materials; using Glamourer.Interop; using Glamourer.Interop.Material; using Glamourer.Interop.Structs; @@ -34,7 +35,8 @@ public class ActorPanel( ImportService _importService, ICondition _conditions, DictModelChara _modelChara, - CustomizeParameterDrawer _parameterDrawer) + CustomizeParameterDrawer _parameterDrawer, + MaterialDrawer _materialDrawer) { private ActorIdentifier _identifier; private string _actorName = string.Empty; @@ -121,16 +123,36 @@ public class ActorPanel( RevertButtons(); + + if (ImGui.CollapsingHeader("Material Shit")) + _materialDrawer.DrawPanel(_actor); 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); + index.TryGetValue(_actor, out var current); + _test = current; if (ImGui.ColorPicker3("TestPicker", ref _test) && _actor.Valid) - MaterialService.Test(_actor, index, _test); + _state.Materials.AddOrUpdateValue(index, new MaterialValueState(current, _test, StateSource.Manual)); + if (ImGui.ColorPicker3("TestPicker2", ref _test) && _actor.Valid) + _state.Materials.AddOrUpdateValue(index, new MaterialValueState(current, _test, StateSource.Fixed)); + + foreach (var value in _state.Materials.Values) + { + var id = MaterialValueIndex.FromKey(value.Key); + ImGui.TextUnformatted($"{id.DrawObject} {id.SlotIndex} {id.MaterialIndex} {id.RowIndex} {id.DataIndex} "); + ImGui.SameLine(0, 0); + var game = ImGui.ColorConvertFloat4ToU32(new Vector4(value.Value.Game, 1)); + ImGuiUtil.DrawTextButton(" ", Vector2.Zero, game); + ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X); + var model = ImGui.ColorConvertFloat4ToU32(new Vector4(value.Value.Model, 1)); + ImGuiUtil.DrawTextButton(" ", Vector2.Zero, model); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" {value.Value.Source}"); + } using var disabled = ImRaii.Disabled(transformationId != 0); if (_state.ModelData.IsHuman) diff --git a/Glamourer/Interop/Material/MaterialService.cs b/Glamourer/Interop/Material/MaterialService.cs index 91635c5..6b49d3d 100644 --- a/Glamourer/Interop/Material/MaterialService.cs +++ b/Glamourer/Interop/Material/MaterialService.cs @@ -1,44 +1,12 @@ -using Dalamud.Interface.Utility.Raii; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +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; @@ -49,7 +17,7 @@ public static unsafe class MaterialService /// 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) + public static bool ReplaceColorTable(Texture** original, in ColorTable colorTable) { if (original == null) return false; @@ -63,7 +31,7 @@ public static unsafe class MaterialService if (texture.IsInvalid) return false; - fixed(ColorTable* ptr = &colorTable) + fixed (ColorTable* ptr = &colorTable) { if (!texture.Texture->InitializeContents(ptr)) return false; @@ -73,6 +41,22 @@ public static unsafe class MaterialService return true; } + public static bool GenerateNewColorTable(in ColorTable colorTable, out Texture* texture) + { + var textureSize = stackalloc int[2]; + textureSize[0] = TextureWidth; + textureSize[1] = TextureHeight; + + texture = Device.Instance()->CreateTexture2D(textureSize, 1, (uint)TexFile.TextureFormat.R16G16B16A16F, + (uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7); + if (texture == null) + return false; + + fixed (ColorTable* ptr = &colorTable) + { + return texture->InitializeContents(ptr); + } + } /// Obtain a pointer to the models pointer to a specific color table texture. /// @@ -112,19 +96,4 @@ public static unsafe class MaterialService 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/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs index 257dfc8..d4aec65 100644 --- a/Glamourer/Interop/Material/MaterialValueManager.cs +++ b/Glamourer/Interop/Material/MaterialValueManager.cs @@ -1,17 +1,27 @@ -namespace Glamourer.Interop.Material; +global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager; +global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager; +using Glamourer.State; -public readonly struct MaterialValueManager + +namespace Glamourer.Interop.Material; + +public record struct MaterialValueState(Vector3 Game, Vector3 Model, StateSource Source); + +public readonly struct MaterialValueManager { - private readonly List<(uint Key, Vector3 Value)> _values = []; + private readonly List<(uint Key, T Value)> _values = []; public MaterialValueManager() { } - public bool TryGetValue(MaterialValueIndex index, out Vector3 value) + public void Clear() + => _values.Clear(); + + public bool TryGetValue(MaterialValueIndex index, out T value) { if (_values.Count == 0) { - value = Vector3.Zero; + value = default!; return false; } @@ -22,11 +32,11 @@ public readonly struct MaterialValueManager return true; } - value = Vector3.Zero; + value = default!; return false; } - public bool TryAddValue(MaterialValueIndex index, in Vector3 value) + public bool TryAddValue(MaterialValueIndex index, in T value) { var key = index.Key; var idx = Search(key); @@ -50,7 +60,7 @@ public readonly struct MaterialValueManager return true; } - public void AddOrUpdateValue(MaterialValueIndex index, in Vector3 value) + public void AddOrUpdateValue(MaterialValueIndex index, in T value) { var key = index.Key; var idx = Search(key); @@ -60,11 +70,11 @@ public readonly struct MaterialValueManager _values[idx] = (key, value); } - public bool UpdateValue(MaterialValueIndex index, in Vector3 value, out Vector3 oldValue) + public bool UpdateValue(MaterialValueIndex index, in T value, out T oldValue) { if (_values.Count == 0) { - oldValue = Vector3.Zero; + oldValue = default!; return false; } @@ -72,7 +82,7 @@ public readonly struct MaterialValueManager var idx = Search(key); if (idx < 0) { - oldValue = Vector3.Zero; + oldValue = default!; return false; } @@ -81,6 +91,9 @@ public readonly struct MaterialValueManager return true; } + public IReadOnlyList<(uint Key, T Value)> Values + => _values; + public int RemoveValues(MaterialValueIndex min, MaterialValueIndex max) { var (minIdx, maxIdx) = GetMinMax(CollectionsMarshal.AsSpan(_values), min.Key, max.Key); @@ -92,21 +105,21 @@ public readonly struct MaterialValueManager return count; } - public ReadOnlySpan<(uint key, Vector3 Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) + public ReadOnlySpan<(uint key, T 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, + public static ReadOnlySpan<(uint Key, T Value)> Filter(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex min, MaterialValueIndex max) { var (minIdx, maxIdx) = GetMinMax(values, min.Key, max.Key); - return minIdx < 0 ? [] : values[minIdx..(maxIdx - minIdx)]; + return minIdx < 0 ? [] : values[minIdx..(maxIdx - minIdx + 1)]; } /// 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) + private static (int MinIdx, int MaxIdx) GetMinMax(ReadOnlySpan<(uint Key, T Value)> values, uint minKey, uint maxKey) { // Find the minimum index by binary search. - var idx = values.BinarySearch((minKey, Vector3.Zero), Comparer.Instance); + var idx = values.BinarySearch((minKey, default!), Comparer.Instance); var minIdx = idx; // If the key does not exist, check if it is an invalid range or set it correctly. @@ -131,7 +144,7 @@ public readonly struct MaterialValueManager // Do pretty much the same but in the other direction with the maximum key. - var maxIdx = values[idx..].BinarySearch((maxKey, Vector3.Zero), Comparer.Instance); + var maxIdx = values[idx..].BinarySearch((maxKey, default!), Comparer.Instance); if (maxIdx < 0) { maxIdx = ~maxIdx; @@ -149,14 +162,14 @@ public readonly struct MaterialValueManager [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private int Search(uint key) - => _values.BinarySearch((key, Vector3.Zero), Comparer.Instance); + => _values.BinarySearch((key, default!), Comparer.Instance); - private class Comparer : IComparer<(uint Key, Vector3 Value)> + private class Comparer : IComparer<(uint Key, T 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) + int IComparer<(uint Key, T Value)>.Compare((uint Key, T Value) x, (uint Key, T Value) y) => x.Key.CompareTo(y.Key); } } diff --git a/Glamourer/Interop/Material/PrepareColorSet.cs b/Glamourer/Interop/Material/PrepareColorSet.cs index 137228e..7cfbf6c 100644 --- a/Glamourer/Interop/Material/PrepareColorSet.cs +++ b/Glamourer/Interop/Material/PrepareColorSet.cs @@ -9,7 +9,6 @@ using Glamourer.State; using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; @@ -58,28 +57,18 @@ public sealed unsafe class PrepareColorSet return _task.Result.Original(characterBase, material, stainId); } - public static MtrlFile.ColorTable GetColorTable(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId) + public static bool TryGetColorTable(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId, out MtrlFile.ColorTable table) { - var table = new MtrlFile.ColorTable(); - characterBase->ReadStainingTemplate(material, stainId.Id, (Half*)(&table)); - return table; - } - - public static bool TryGetColorTable(Model model, byte slotIdx, out MtrlFile.ColorTable table) - { - var table2 = new MtrlFile.ColorTable(); - if (!model.IsCharacterBase || slotIdx < model.AsCharacterBase->SlotCount) - return false; - - var resource = (MaterialResourceHandle*)model.AsCharacterBase->Materials[slotIdx]; - var stain = model.AsCharacterBase->GetModelType() switch + if (material->ColorTable == null) { - CharacterBase.ModelType.Human => model.GetArmor(EquipSlotExtensions.ToEquipSlot(slotIdx)).Stain, - CharacterBase.ModelType.Weapon => (StainId)model.AsWeapon->ModelUnknown, - _ => (StainId)0, - }; - model.AsCharacterBase->ReadStainingTemplate(resource, stain.Id, (Half*)(&table2)); - table = table2; + table = default; + return false; + } + + var newTable = *(MtrlFile.ColorTable*)material->ColorTable; + if(stainId.Id != 0) + characterBase->ReadStainingTemplate(material, stainId.Id, (Half*)(&newTable)); + table = newTable; return true; } } @@ -111,10 +100,6 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable 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}."); - var table = PrepareColorSet.GetColorTable(characterBase, material, stain); - Glamourer.Log.Information($"{table[15].Diffuse}"); if (!validType || slotId == byte.MaxValue @@ -122,12 +107,49 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable || !_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) - ; + var values = state.Materials.GetValues(min, max); + if (values.Length == 0) + return; + + if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet)) + return; + + for (var i = 0; i < values.Length; ++i) + { + var idx = MaterialValueIndex.FromKey(values[i].key); + var (oldGame, model, source) = values[i].Value; + ref var row = ref baseColorSet[idx.RowIndex]; + if (!idx.DataIndex.TryGetValue(row, out var newGame)) + continue; + + if (newGame == oldGame) + { + idx.DataIndex.SetValue(ref row, model); + } + else + { + switch (source) + { + case StateSource.Manual: + case StateSource.Pending: + state.Materials.RemoveValue(idx); + --i; + break; + case StateSource.Ipc: + case StateSource.Fixed: + idx.DataIndex.SetValue(ref row, model); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, model, source), out _); + break; + + } + } + } + + if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture)) + ret = (nint)texture; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index b5126da..5d582f6 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -30,6 +30,9 @@ public class ActorState /// The territory the draw object was created last. public ushort LastTerritory; + /// State for specific material values. + public readonly StateMaterialManager Materials = new(); + /// Whether the State is locked at all. public bool IsLocked => Combination != 0; @@ -84,4 +87,4 @@ public class ActorState LastTerritory = territory; return true; } -} \ No newline at end of file +} diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 2643271..c6658c5 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -240,6 +240,8 @@ public sealed class StateManager( foreach (var flag in CustomizeParameterExtensions.AllFlags) state.Sources[flag] = StateSource.Game; + state.Materials.Clear(); + var actors = ActorData.Invalid; if (source is StateSource.Manual or StateSource.Ipc) actors = Applier.ApplyAll(state, redraw, true); From 962c4e53ad3a114ce6adeb22142d405f3f5ddfed Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jan 2024 16:04:56 +0100 Subject: [PATCH 05/17] Better handling of application rules. --- Glamourer/Api/GlamourerIpc.Events.cs | 5 +- .../Api/GlamourerIpc.GetCustomization.cs | 3 +- Glamourer/Designs/ApplicationRules.cs | 84 +++++++++++++++++++ Glamourer/Designs/DesignBase.cs | 15 +++- Glamourer/Designs/DesignConverter.cs | 40 ++++----- Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 12 +-- Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs | 15 ++-- Glamourer/Services/CommandService.cs | 7 +- 8 files changed, 130 insertions(+), 51 deletions(-) create mode 100644 Glamourer/Designs/ApplicationRules.cs diff --git a/Glamourer/Api/GlamourerIpc.Events.cs b/Glamourer/Api/GlamourerIpc.Events.cs index 8caa495..982e4a1 100644 --- a/Glamourer/Api/GlamourerIpc.Events.cs +++ b/Glamourer/Api/GlamourerIpc.Events.cs @@ -1,4 +1,5 @@ -using Glamourer.Events; +using Glamourer.Designs; +using Glamourer.Events; using Glamourer.Interop.Structs; using Glamourer.State; using Penumbra.Api.Helpers; @@ -18,7 +19,7 @@ public partial class GlamourerIpc private void OnStateChanged(StateChanged.Type type, StateSource source, ActorState state, ActorData actors, object? data = null) { foreach (var actor in actors.Objects) - _stateChangedProvider.Invoke(type, actor.Address, new Lazy(() => _designConverter.ShareBase64(state))); + _stateChangedProvider.Invoke(type, actor.Address, new Lazy(() => _designConverter.ShareBase64(state, ApplicationRules.AllButParameters(state)))); } private void OnGPoseChanged(bool value) diff --git a/Glamourer/Api/GlamourerIpc.GetCustomization.cs b/Glamourer/Api/GlamourerIpc.GetCustomization.cs index bedf514..4f35a2f 100644 --- a/Glamourer/Api/GlamourerIpc.GetCustomization.cs +++ b/Glamourer/Api/GlamourerIpc.GetCustomization.cs @@ -1,5 +1,6 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; +using Glamourer.Designs; using Penumbra.Api.Helpers; using Penumbra.GameData.Actors; @@ -40,6 +41,6 @@ public partial class GlamourerIpc return null; } - return _designConverter.ShareBase64(state); + return _designConverter.ShareBase64(state, ApplicationRules.AllButParameters(state)); } } diff --git a/Glamourer/Designs/ApplicationRules.cs b/Glamourer/Designs/ApplicationRules.cs new file mode 100644 index 0000000..8fdd5d6 --- /dev/null +++ b/Glamourer/Designs/ApplicationRules.cs @@ -0,0 +1,84 @@ +using Glamourer.GameData; +using Glamourer.State; +using ImGuiNET; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs; + +public readonly struct ApplicationRules( + EquipFlag equip, + CustomizeFlag customize, + CrestFlag crest, + CustomizeParameterFlag parameters, + MetaFlag meta) +{ + public static readonly ApplicationRules All = new(EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, + CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All); + + public static ApplicationRules FromModifiers(ActorState state) + => FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift); + + public static ApplicationRules NpcFromModifiers() + => NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift); + + public static ApplicationRules AllButParameters(ActorState state) + => new(All.Equip, All.Customize, All.Crest, ComputeParameters(state.ModelData, state.BaseData, All.Parameters), All.Meta); + + public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift) + => new(ctrl || !shift ? EquipFlagExtensions.All : 0, + !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0, + 0, + 0, + ctrl || !shift ? MetaFlag.VisorState : 0); + + public static ApplicationRules FromModifiers(ActorState state, bool ctrl, bool shift) + { + var equip = ctrl || !shift ? EquipFlagExtensions.All : 0; + var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0; + var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant; + var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All; + var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0; + if (equip != 0) + meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState; + + return new ApplicationRules(equip, customize, crest, ComputeParameters(state.ModelData, state.BaseData, parameters), meta); + } + + public void Apply(DesignBase design) + { + design.ApplyEquip = Equip; + design.ApplyCustomize = Customize; + design.ApplyCrest = Crest; + design.ApplyParameters = Parameters; + design.ApplyMeta = Meta; + } + + public EquipFlag Equip + => equip & EquipFlagExtensions.All; + + public CustomizeFlag Customize + => customize & CustomizeFlagExtensions.AllRelevant; + + public CrestFlag Crest + => crest & CrestExtensions.AllRelevant; + + public CustomizeParameterFlag Parameters + => parameters & CustomizeParameterExtensions.All; + + public MetaFlag Meta + => meta & MetaExtensions.All; + + public static CustomizeParameterFlag ComputeParameters(in DesignData model, in DesignData game, + CustomizeParameterFlag baseFlags = CustomizeParameterExtensions.All) + { + foreach (var flag in baseFlags.Iterate()) + { + var modelValue = model.Parameters[flag]; + var gameValue = game.Parameters[flag]; + if ((modelValue.InternalQuadruple - gameValue.InternalQuadruple).LengthSquared() > 1e-9f) + baseFlags &= ~flag; + } + + return baseFlags; + } +} diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index a533bc4..9699bc2 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -1,7 +1,7 @@ using Dalamud.Interface.Internal.Notifications; using Glamourer.GameData; +using Glamourer.Interop.Material; using Glamourer.Services; -using Glamourer.State; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.GameData.Enums; @@ -14,7 +14,16 @@ public class DesignBase { public const int FileVersion = 1; - private DesignData _designData = new(); + private DesignData _designData = new(); + private readonly DesignMaterialManager _materials = new(); + + /// For read-only information about custom material color changes. + public IReadOnlyList<(uint, MaterialValueDesign)> Materials + => _materials.Values; + + /// To make it clear something is edited here. + public DesignMaterialManager GetMaterialDataRef() + => _materials; /// For read-only information about the actual design. public ref readonly DesignData DesignData @@ -30,6 +39,7 @@ public class DesignBase CustomizeSet = SetCustomizationSet(customize); } + /// Used when importing .cma or .chara files. internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags) { _designData = designData; @@ -42,6 +52,7 @@ public class DesignBase internal DesignBase(DesignBase clone) { _designData = clone._designData; + _materials = clone._materials.Clone(); CustomizeSet = clone.CustomizeSet; ApplyCustomize = clone.ApplyCustomizeRaw; ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All; diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs index 14c85f6..be70672 100644 --- a/Glamourer/Designs/DesignConverter.cs +++ b/Glamourer/Designs/DesignConverter.cs @@ -1,5 +1,4 @@ using Glamourer.Designs.Links; -using Glamourer.GameData; using Glamourer.Services; using Glamourer.State; using Glamourer.Utility; @@ -11,7 +10,12 @@ using Penumbra.GameData.Structs; namespace Glamourer.Designs; -public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizeService _customize, HumanModelList _humans, DesignLinkLoader _linkLoader) +public class DesignConverter( + ItemManager _items, + DesignManager _designs, + CustomizeService _customize, + HumanModelList _humans, + DesignLinkLoader _linkLoader) { public const byte Version = 6; @@ -21,9 +25,9 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi public JObject ShareJObject(Design design) => design.JsonSerialize(); - public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags) + public JObject ShareJObject(ActorState state, in ApplicationRules rules) { - var design = Convert(state, equipFlags, customizeFlags, crestFlags, parameterFlags); + var design = Convert(state, rules); return ShareJObject(design); } @@ -33,32 +37,22 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi public string ShareBase64(DesignBase design) => ShareBase64(ShareJObject(design)); - public string ShareBase64(ActorState state) - => ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All, CrestExtensions.All, CustomizeParameterExtensions.All); + public string ShareBase64(ActorState state, in ApplicationRules rules) + => ShareBase64(state.ModelData, rules); - public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags) - => ShareBase64(state.ModelData, equipFlags, customizeFlags, crestFlags, parameterFlags); - - public string ShareBase64(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags) + public string ShareBase64(in DesignData data, in ApplicationRules rules) { - var design = Convert(data, equipFlags, customizeFlags, crestFlags, parameterFlags); + var design = Convert(data, rules); return ShareBase64(ShareJObject(design)); } - public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags) - => Convert(state.ModelData, equipFlags, customizeFlags, crestFlags, parameterFlags); + public DesignBase Convert(ActorState state, in ApplicationRules rules) + => Convert(state.ModelData, rules); - public DesignBase Convert(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags, CustomizeParameterFlag parameterFlags) + public DesignBase Convert(in DesignData data, in ApplicationRules rules) { var design = _designs.CreateTemporary(); - design.ApplyEquip = equipFlags & EquipFlagExtensions.All; - design.ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; - design.ApplyCrest = crestFlags & CrestExtensions.All; - design.ApplyParameters = parameterFlags & CustomizeParameterExtensions.All; - design.SetApplyMeta(MetaIndex.HatState, design.DoApplyEquip(EquipSlot.Head)); - design.SetApplyMeta(MetaIndex.VisorState, design.DoApplyEquip(EquipSlot.Head)); - design.SetApplyMeta(MetaIndex.WeaponState, design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand)); - design.SetApplyMeta(MetaIndex.Wetness, true); + rules.Apply(design); design.SetDesignData(_customize, data); return design; } @@ -139,7 +133,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi return ret; } - private static string ShareBase64(JObject jObject) + private static string ShareBase64(JToken jObject) { var json = jObject.ToString(Formatting.None); var compressed = json.Compress(Version); diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 6ec62f5..94f4b56 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -356,8 +356,7 @@ public class ActorPanel( { ImGui.OpenPopup("Save as Design"); _newName = _state!.Identifier.ToName(); - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); - _newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest, applyParameters); + _newDesign = _converter.Convert(_state, ApplicationRules.FromModifiers(_state)); } private void SaveDesignDrawPopup() @@ -392,8 +391,7 @@ public class ActorPanel( { try { - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); - var text = _converter.ShareBase64(_state!, applyGear, applyCustomize, applyCrest, applyParameters); + var text = _converter.ShareBase64(_state!, ApplicationRules.FromModifiers(_state!)); ImGui.SetClipboardText(text); } catch (Exception ex) @@ -432,9 +430,8 @@ public class ActorPanel( !data.Valid || id == _identifier || _state!.ModelData.ModelId != 0)) return; - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) - _stateManager.ApplyDesign(state, _converter.Convert(_state!, applyGear, applyCustomize, applyCrest, applyParameters), + _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), ApplySettings.Manual); } @@ -450,9 +447,8 @@ public class ActorPanel( !data.Valid || id == _identifier || _state!.ModelData.ModelId != 0)) return; - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) - _stateManager.ApplyDesign(state, _converter.Convert(_state!, applyGear, applyCustomize, applyCrest, applyParameters), + _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), ApplySettings.Manual); } } diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs index bd04a57..77dafa9 100644 --- a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs +++ b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs @@ -84,9 +84,8 @@ public class NpcPanel( { try { - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); var data = ToDesignData(); - var text = _converter.ShareBase64(data, applyGear, applyCustomize, applyCrest, applyParameters); + var text = _converter.ShareBase64(data, ApplicationRules.NpcFromModifiers()); ImGui.SetClipboardText(text); } catch (Exception ex) @@ -100,11 +99,9 @@ public class NpcPanel( private void SaveDesignOpen() { ImGui.OpenPopup("Save as Design"); - _newName = _selector.Selection.Name; - var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags(); - + _newName = _selector.Selection.Name; var data = ToDesignData(); - _newDesign = _converter.Convert(data, applyGear, applyCustomize, applyCrest, applyParameters); + _newDesign = _converter.Convert(data, ApplicationRules.NpcFromModifiers()); } private void SaveDesignDrawPopup() @@ -198,8 +195,7 @@ public class NpcPanel( if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var (applyGear, applyCustomize, _, _) = UiHelpers.ConvertKeysToFlags(); - var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, 0, 0); + var design = _converter.Convert(ToDesignData(), ApplicationRules.NpcFromModifiers()); _state.ApplyDesign(state, design, ApplySettings.Manual); } } @@ -217,8 +213,7 @@ public class NpcPanel( if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var (applyGear, applyCustomize, _, _) = UiHelpers.ConvertKeysToFlags(); - var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, 0, 0); + var design = _converter.Convert(ToDesignData(), ApplicationRules.NpcFromModifiers()); _state.ApplyDesign(state, design, ApplySettings.Manual); } } diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index eb0e792..895ad2f 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -3,8 +3,6 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using Glamourer.Automation; using Glamourer.Designs; -using Glamourer.Events; -using Glamourer.GameData; using Glamourer.Gui; using Glamourer.Interop; using Glamourer.Interop.Penumbra; @@ -509,7 +507,7 @@ public class CommandService : IDisposable try { - var text = _converter.ShareBase64(state); + var text = _converter.ShareBase64(state, ApplicationRules.AllButParameters(state)); ImGui.SetClipboardText(text); return true; } @@ -548,8 +546,7 @@ public class CommandService : IDisposable && _stateManager.GetOrCreate(identifier, data.Objects[0], out state))) continue; - var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All, - CustomizeParameterExtensions.All); + var design = _converter.Convert(state, ApplicationRules.FromModifiers(state)); _designManager.CreateClone(design, split[0], true); return true; } From 502b2439b40dbc3a9fb72fbe8fed00c25bc8aacb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jan 2024 16:05:07 +0100 Subject: [PATCH 06/17] Fix application rule display for meta. --- Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index 93ebad5..a957975 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -293,10 +293,10 @@ public class DesignPanel( var labels = new[] { + "Apply Wetness", "Apply Hat Visibility", "Apply Visor State", "Apply Weapon Visibility", - "Apply Wetness", }; foreach (var (index, label) in MetaExtensions.AllRelevant.Zip(labels)) From eea4de63d54ae735ac5dc172254a1b0db68fa510 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jan 2024 16:09:30 +0100 Subject: [PATCH 07/17] Fix inverted logic. --- Glamourer/Designs/ApplicationRules.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Glamourer/Designs/ApplicationRules.cs b/Glamourer/Designs/ApplicationRules.cs index 8fdd5d6..1852c7e 100644 --- a/Glamourer/Designs/ApplicationRules.cs +++ b/Glamourer/Designs/ApplicationRules.cs @@ -75,7 +75,7 @@ public readonly struct ApplicationRules( { var modelValue = model.Parameters[flag]; var gameValue = game.Parameters[flag]; - if ((modelValue.InternalQuadruple - gameValue.InternalQuadruple).LengthSquared() > 1e-9f) + if ((modelValue.InternalQuadruple - gameValue.InternalQuadruple).LengthSquared() < 1e-9f) baseFlags &= ~flag; } From 994b7bfb6c08196a6e5470eb50b223ebb4dd4688 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jan 2024 16:09:58 +0100 Subject: [PATCH 08/17] Add clone function to MaterialValueManager. --- Glamourer/Interop/Material/MaterialValueManager.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Glamourer/Interop/Material/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs index d4aec65..5dd3001 100644 --- a/Glamourer/Interop/Material/MaterialValueManager.cs +++ b/Glamourer/Interop/Material/MaterialValueManager.cs @@ -1,10 +1,11 @@ global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager; -global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager; +global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager; using Glamourer.State; namespace Glamourer.Interop.Material; +public record struct MaterialValueDesign(Vector3 Value, bool Enabled); public record struct MaterialValueState(Vector3 Game, Vector3 Model, StateSource Source); public readonly struct MaterialValueManager @@ -17,6 +18,13 @@ public readonly struct MaterialValueManager public void Clear() => _values.Clear(); + public MaterialValueManager Clone() + { + var ret = new MaterialValueManager(); + ret._values.AddRange(_values); + return ret; + } + public bool TryGetValue(MaterialValueIndex index, out T value) { if (_values.Count == 0) From 818bf710329b9fa1a10e126e4ae7ffad8979d230 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jan 2024 17:51:52 +0100 Subject: [PATCH 09/17] Add IpcManual state. --- Glamourer/Api/GlamourerIpc.Apply.cs | 49 ++++++++++++++++--- Glamourer/Api/GlamourerIpc.Revert.cs | 3 +- Glamourer/Api/GlamourerIpc.Set.cs | 24 ++++++--- Glamourer/Api/GlamourerIpc.cs | 31 +++++++++--- Glamourer/Configuration.cs | 1 + .../Gui/Tabs/DebugTab/ActiveStatePanel.cs | 1 - Glamourer/Gui/Tabs/DebugTab/IpcTesterPanel.cs | 46 +++++++++++++++++ .../Gui/Tabs/DesignTab/DesignDetailTab.cs | 9 +++- Glamourer/Gui/Tabs/SettingsTab.cs | 5 ++ Glamourer/Interop/Material/PrepareColorSet.cs | 5 +- Glamourer/State/StateApplier.cs | 10 ++-- Glamourer/State/StateEditor.cs | 46 +++++++++-------- Glamourer/State/StateListener.cs | 49 ++++++++++--------- Glamourer/State/StateManager.cs | 28 ++--------- Glamourer/State/StateSource.cs | 44 +++++++++++++++-- 15 files changed, 242 insertions(+), 109 deletions(-) diff --git a/Glamourer/Api/GlamourerIpc.Apply.cs b/Glamourer/Api/GlamourerIpc.Apply.cs index d155cf2..f09d491 100644 --- a/Glamourer/Api/GlamourerIpc.Apply.cs +++ b/Glamourer/Api/GlamourerIpc.Apply.cs @@ -11,7 +11,9 @@ namespace Glamourer.Api; public partial class GlamourerIpc { public const string LabelApplyAll = "Glamourer.ApplyAll"; + public const string LabelApplyAllOnce = "Glamourer.ApplyAllOnce"; public const string LabelApplyAllToCharacter = "Glamourer.ApplyAllToCharacter"; + public const string LabelApplyAllOnceToCharacter = "Glamourer.ApplyAllOnceToCharacter"; public const string LabelApplyOnlyEquipment = "Glamourer.ApplyOnlyEquipment"; public const string LabelApplyOnlyEquipmentToCharacter = "Glamourer.ApplyOnlyEquipmentToCharacter"; public const string LabelApplyOnlyCustomization = "Glamourer.ApplyOnlyCustomization"; @@ -24,11 +26,15 @@ public partial class GlamourerIpc public const string LabelApplyOnlyCustomizationLock = "Glamourer.ApplyOnlyCustomizationLock"; public const string LabelApplyOnlyCustomizationToCharacterLock = "Glamourer.ApplyOnlyCustomizationToCharacterLock"; - public const string LabelApplyByGuid = "Glamourer.ApplyByGuid"; - public const string LabelApplyByGuidToCharacter = "Glamourer.ApplyByGuidToCharacter"; + public const string LabelApplyByGuid = "Glamourer.ApplyByGuid"; + public const string LabelApplyByGuidOnce = "Glamourer.ApplyByGuidOnce"; + public const string LabelApplyByGuidToCharacter = "Glamourer.ApplyByGuidToCharacter"; + public const string LabelApplyByGuidOnceToCharacter = "Glamourer.ApplyByGuidOnceToCharacter"; private readonly ActionProvider _applyAllProvider; + private readonly ActionProvider _applyAllOnceProvider; private readonly ActionProvider _applyAllToCharacterProvider; + private readonly ActionProvider _applyAllOnceToCharacterProvider; private readonly ActionProvider _applyOnlyEquipmentProvider; private readonly ActionProvider _applyOnlyEquipmentToCharacterProvider; private readonly ActionProvider _applyOnlyCustomizationProvider; @@ -42,14 +48,22 @@ public partial class GlamourerIpc private readonly ActionProvider _applyOnlyCustomizationToCharacterProviderLock; private readonly ActionProvider _applyByGuidProvider; + private readonly ActionProvider _applyByGuidOnceProvider; private readonly ActionProvider _applyByGuidToCharacterProvider; + private readonly ActionProvider _applyByGuidOnceToCharacterProvider; public static ActionSubscriber ApplyAllSubscriber(DalamudPluginInterface pi) => new(pi, LabelApplyAll); + public static ActionSubscriber ApplyAllOnceSubscriber(DalamudPluginInterface pi) + => new(pi, LabelApplyAllOnce); + public static ActionSubscriber ApplyAllToCharacterSubscriber(DalamudPluginInterface pi) => new(pi, LabelApplyAllToCharacter); + public static ActionSubscriber ApplyAllOnceToCharacterSubscriber(DalamudPluginInterface pi) + => new(pi, LabelApplyAllOnceToCharacter); + public static ActionSubscriber ApplyOnlyEquipmentSubscriber(DalamudPluginInterface pi) => new(pi, LabelApplyOnlyEquipment); @@ -65,15 +79,27 @@ public partial class GlamourerIpc public static ActionSubscriber ApplyByGuidSubscriber(DalamudPluginInterface pi) => new(pi, LabelApplyByGuid); + public static ActionSubscriber ApplyByGuidOnceSubscriber(DalamudPluginInterface pi) + => new(pi, LabelApplyByGuidOnce); + public static ActionSubscriber ApplyByGuidToCharacterSubscriber(DalamudPluginInterface pi) => new(pi, LabelApplyByGuidToCharacter); + public static ActionSubscriber ApplyByGuidOnceToCharacterSubscriber(DalamudPluginInterface pi) + => new(pi, LabelApplyByGuidOnceToCharacter); + public void ApplyAll(string base64, string characterName) => ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, 0); + public void ApplyAllOnce(string base64, string characterName) + => ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, 0, true); + public void ApplyAllToCharacter(string base64, Character? character) => ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, 0); + public void ApplyAllOnceToCharacter(string base64, Character? character) + => ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, 0, true); + public void ApplyOnlyEquipment(string base64, string characterName) => ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(characterName), version, 0); @@ -107,12 +133,18 @@ public partial class GlamourerIpc public void ApplyByGuid(Guid identifier, string characterName) - => ApplyDesignByGuid(identifier, FindActors(characterName), 0); + => ApplyDesignByGuid(identifier, FindActors(characterName), 0, false); + + public void ApplyByGuidOnce(Guid identifier, string characterName) + => ApplyDesignByGuid(identifier, FindActors(characterName), 0, true); public void ApplyByGuidToCharacter(Guid identifier, Character? character) - => ApplyDesignByGuid(identifier, FindActors(character), 0); + => ApplyDesignByGuid(identifier, FindActors(character), 0, false); - private void ApplyDesign(DesignBase? design, IEnumerable actors, byte version, uint lockCode) + public void ApplyByGuidOnceToCharacter(Guid identifier, Character? character) + => ApplyDesignByGuid(identifier, FindActors(character), 0, true); + + private void ApplyDesign(DesignBase? design, IEnumerable actors, byte version, uint lockCode, bool once = false) { if (design == null) return; @@ -130,12 +162,13 @@ public partial class GlamourerIpc if ((hasModelId || state.ModelData.ModelId == 0) && state.CanUnlock(lockCode)) { - _stateManager.ApplyDesign(state, design, new ApplySettings(Source:StateSource.Ipc, Key:lockCode)); + _stateManager.ApplyDesign(state, design, + new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: lockCode)); state.Lock(lockCode); } } } - private void ApplyDesignByGuid(Guid identifier, IEnumerable actors, uint lockCode) - => ApplyDesign(_designManager.Designs.ByIdentifier(identifier), actors, DesignConverter.Version, lockCode); + private void ApplyDesignByGuid(Guid identifier, IEnumerable actors, uint lockCode, bool once) + => ApplyDesign(_designManager.Designs.ByIdentifier(identifier), actors, DesignConverter.Version, lockCode, once); } diff --git a/Glamourer/Api/GlamourerIpc.Revert.cs b/Glamourer/Api/GlamourerIpc.Revert.cs index 44a03aa..c5ca3b3 100644 --- a/Glamourer/Api/GlamourerIpc.Revert.cs +++ b/Glamourer/Api/GlamourerIpc.Revert.cs @@ -1,6 +1,5 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; -using Glamourer.Events; using Glamourer.State; using Penumbra.Api.Helpers; using Penumbra.GameData.Actors; @@ -83,7 +82,7 @@ public partial class GlamourerIpc foreach (var id in actors) { if (_stateManager.TryGetValue(id, out var state)) - _stateManager.ResetState(state, StateSource.Ipc, lockCode); + _stateManager.ResetState(state, StateSource.IpcFixed, lockCode); } } diff --git a/Glamourer/Api/GlamourerIpc.Set.cs b/Glamourer/Api/GlamourerIpc.Set.cs index db5941f..93428da 100644 --- a/Glamourer/Api/GlamourerIpc.Set.cs +++ b/Glamourer/Api/GlamourerIpc.Set.cs @@ -20,20 +20,30 @@ public partial class GlamourerIpc ItemInvalid, } - public const string LabelSetItem = "Glamourer.SetItem"; - public const string LabelSetItemByActorName = "Glamourer.SetItemByActorName"; + public const string LabelSetItem = "Glamourer.SetItem"; + public const string LabelSetItemOnce = "Glamourer.SetItemOnce"; + public const string LabelSetItemByActorName = "Glamourer.SetItemByActorName"; + public const string LabelSetItemOnceByActorName = "Glamourer.SetItemOnceByActorName"; private readonly FuncProvider _setItemProvider; + private readonly FuncProvider _setItemOnceProvider; private readonly FuncProvider _setItemByActorNameProvider; + private readonly FuncProvider _setItemOnceByActorNameProvider; public static FuncSubscriber SetItemSubscriber(DalamudPluginInterface pi) => new(pi, LabelSetItem); + public static FuncSubscriber SetItemOnceSubscriber(DalamudPluginInterface pi) + => new(pi, LabelSetItemOnce); + public static FuncSubscriber SetItemByActorNameSubscriber(DalamudPluginInterface pi) => new(pi, LabelSetItemByActorName); - private GlamourerErrorCode SetItem(Character? character, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key) + public static FuncSubscriber SetItemOnceByActorNameSubscriber(DalamudPluginInterface pi) + => new(pi, LabelSetItemOnceByActorName); + + private GlamourerErrorCode SetItem(Character? character, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key, bool once) { if (itemId.Id == 0) itemId = ItemManager.NothingId(slot); @@ -57,11 +67,12 @@ public partial class GlamourerIpc if (!state.ModelData.IsHuman) return GlamourerErrorCode.ActorNotHuman; - _stateManager.ChangeEquip(state, slot, item, stainId, new ApplySettings(Source: StateSource.Ipc, Key:key)); + _stateManager.ChangeEquip(state, slot, item, stainId, + new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key)); return GlamourerErrorCode.Success; } - private GlamourerErrorCode SetItemByActorName(string name, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key) + private GlamourerErrorCode SetItemByActorName(string name, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key, bool once) { if (itemId.Id == 0) itemId = ItemManager.NothingId(slot); @@ -84,7 +95,8 @@ public partial class GlamourerIpc if (!state.ModelData.IsHuman) return GlamourerErrorCode.ActorNotHuman; - _stateManager.ChangeEquip(state, slot, item, stainId, new ApplySettings(Source: StateSource.Ipc, Key: key)); + _stateManager.ChangeEquip(state, slot, item, stainId, + new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key)); found = true; } diff --git a/Glamourer/Api/GlamourerIpc.cs b/Glamourer/Api/GlamourerIpc.cs index 36d248b..550b7e2 100644 --- a/Glamourer/Api/GlamourerIpc.cs +++ b/Glamourer/Api/GlamourerIpc.cs @@ -46,9 +46,11 @@ public sealed partial class GlamourerIpc : IDisposable _getAllCustomizationFromCharacterProvider = new FuncProvider(pi, LabelGetAllCustomizationFromCharacter, GetAllCustomizationFromCharacter); - _applyAllProvider = new ActionProvider(pi, LabelApplyAll, ApplyAll); - _applyAllToCharacterProvider = new ActionProvider(pi, LabelApplyAllToCharacter, ApplyAllToCharacter); - _applyOnlyEquipmentProvider = new ActionProvider(pi, LabelApplyOnlyEquipment, ApplyOnlyEquipment); + _applyAllProvider = new ActionProvider(pi, LabelApplyAll, ApplyAll); + _applyAllOnceProvider = new ActionProvider(pi, LabelApplyAll, ApplyAllOnce); + _applyAllToCharacterProvider = new ActionProvider(pi, LabelApplyAllToCharacter, ApplyAllToCharacter); + _applyAllOnceToCharacterProvider = new ActionProvider(pi, LabelApplyAllToCharacter, ApplyAllOnceToCharacter); + _applyOnlyEquipmentProvider = new ActionProvider(pi, LabelApplyOnlyEquipment, ApplyOnlyEquipment); _applyOnlyEquipmentToCharacterProvider = new ActionProvider(pi, LabelApplyOnlyEquipmentToCharacter, ApplyOnlyEquipmentToCharacter); _applyOnlyCustomizationProvider = new ActionProvider(pi, LabelApplyOnlyCustomization, ApplyOnlyCustomization); @@ -66,8 +68,11 @@ public sealed partial class GlamourerIpc : IDisposable _applyOnlyCustomizationToCharacterProviderLock = new ActionProvider(pi, LabelApplyOnlyCustomizationToCharacterLock, ApplyOnlyCustomizationToCharacterLock); - _applyByGuidProvider = new ActionProvider(pi, LabelApplyByGuid, ApplyByGuid); + _applyByGuidProvider = new ActionProvider(pi, LabelApplyByGuid, ApplyByGuid); + _applyByGuidOnceProvider = new ActionProvider(pi, LabelApplyByGuidOnce, ApplyByGuidOnce); _applyByGuidToCharacterProvider = new ActionProvider(pi, LabelApplyByGuidToCharacter, ApplyByGuidToCharacter); + _applyByGuidOnceToCharacterProvider = + new ActionProvider(pi, LabelApplyByGuidOnceToCharacter, ApplyByGuidOnceToCharacter); _revertProvider = new ActionProvider(pi, LabelRevert, Revert); _revertCharacterProvider = new ActionProvider(pi, LabelRevertCharacter, RevertCharacter); @@ -83,9 +88,14 @@ public sealed partial class GlamourerIpc : IDisposable _gPoseChangedProvider = new EventProvider(pi, LabelGPoseChanged); _setItemProvider = new FuncProvider(pi, LabelSetItem, - (idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key)); - _setItemByActorNameProvider = new FuncProvider(pi, LabelSetItemByActorName, - (name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key)); + (idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key, false)); + _setItemOnceProvider = new FuncProvider(pi, LabelSetItem, + (idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key, true)); + + _setItemByActorNameProvider = new FuncProvider(pi, LabelSetItemOnceByActorName, + (name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key, false)); + _setItemOnceByActorNameProvider = new FuncProvider(pi, LabelSetItemOnceByActorName, + (name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key, true)); _stateChangedEvent.Subscribe(OnStateChanged, StateChanged.Priority.GlamourerIpc); _gPose.Subscribe(OnGPoseChanged, GPoseService.Priority.GlamourerIpc); @@ -102,7 +112,9 @@ public sealed partial class GlamourerIpc : IDisposable _getAllCustomizationFromCharacterProvider.Dispose(); _applyAllProvider.Dispose(); + _applyAllOnceProvider.Dispose(); _applyAllToCharacterProvider.Dispose(); + _applyAllOnceToCharacterProvider.Dispose(); _applyOnlyEquipmentProvider.Dispose(); _applyOnlyEquipmentToCharacterProvider.Dispose(); _applyOnlyCustomizationProvider.Dispose(); @@ -113,8 +125,11 @@ public sealed partial class GlamourerIpc : IDisposable _applyOnlyEquipmentToCharacterProviderLock.Dispose(); _applyOnlyCustomizationProviderLock.Dispose(); _applyOnlyCustomizationToCharacterProviderLock.Dispose(); + _applyByGuidProvider.Dispose(); + _applyByGuidOnceProvider.Dispose(); _applyByGuidToCharacterProvider.Dispose(); + _applyByGuidOnceToCharacterProvider.Dispose(); _revertProvider.Dispose(); _revertCharacterProvider.Dispose(); @@ -133,7 +148,9 @@ public sealed partial class GlamourerIpc : IDisposable _getDesignListProvider.Dispose(); _setItemProvider.Dispose(); + _setItemOnceProvider.Dispose(); _setItemByActorNameProvider.Dispose(); + _setItemOnceByActorNameProvider.Dispose(); } private IEnumerable FindActors(string actorName) diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index 78d1a3b..29b0646 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -42,6 +42,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseRgbForColors { get; set; } = true; public bool ShowColorConfig { get; set; } = true; public bool ChangeEntireItem { get; set; } = false; + public bool AlwaysApplyAssociatedMods { get; set; } = false; public ModifiableHotkey ToggleQuickDesignBar { get; set; } = new(VirtualKey.NO_KEY); public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; diff --git a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs index 7b3f594..00df06b 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs @@ -1,7 +1,6 @@ using Dalamud.Interface; using Glamourer.GameData; using Glamourer.Designs; -using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Structs; using Glamourer.State; diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTesterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTesterPanel.cs index 02348a2..d447f9f 100644 --- a/Glamourer/Gui/Tabs/DebugTab/IpcTesterPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTesterPanel.cs @@ -28,7 +28,9 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag private string _base64Apply = string.Empty; private string _designIdentifier = string.Empty; private GlamourerIpc.GlamourerErrorCode _setItemEc; + private GlamourerIpc.GlamourerErrorCode _setItemOnceEc; private GlamourerIpc.GlamourerErrorCode _setItemByActorNameEc; + private GlamourerIpc.GlamourerErrorCode _setItemOnceByActorNameEc; public unsafe void Draw() { @@ -77,12 +79,23 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag if (ImGui.Button("Apply##AllName")) GlamourerIpc.ApplyAllSubscriber(_pluginInterface).Invoke(_base64Apply, _gameObjectName); + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyAllOnce); + ImGui.TableNextColumn(); + if (ImGui.Button("Apply Once##AllName")) + GlamourerIpc.ApplyAllOnceSubscriber(_pluginInterface).Invoke(_base64Apply, _gameObjectName); + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyAllToCharacter); ImGui.TableNextColumn(); if (ImGui.Button("Apply##AllCharacter")) GlamourerIpc.ApplyAllToCharacterSubscriber(_pluginInterface) .Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character); + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyAllOnceToCharacter); + ImGui.TableNextColumn(); + if (ImGui.Button("Apply Once##AllCharacter")) + GlamourerIpc.ApplyAllOnceToCharacterSubscriber(_pluginInterface) + .Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character); + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyOnlyEquipment); ImGui.TableNextColumn(); if (ImGui.Button("Apply##EquipName")) @@ -111,12 +124,23 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag if (ImGui.Button("Apply##ByGuidName") && Guid.TryParse(_designIdentifier, out var guid1)) GlamourerIpc.ApplyByGuidSubscriber(_pluginInterface).Invoke(guid1, _gameObjectName); + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidOnce); + ImGui.TableNextColumn(); + if (ImGui.Button("Apply Once##ByGuidName") && Guid.TryParse(_designIdentifier, out var guid1Once)) + GlamourerIpc.ApplyByGuidOnceSubscriber(_pluginInterface).Invoke(guid1Once, _gameObjectName); + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidToCharacter); ImGui.TableNextColumn(); if (ImGui.Button("Apply##ByGuidCharacter") && Guid.TryParse(_designIdentifier, out var guid2)) GlamourerIpc.ApplyByGuidToCharacterSubscriber(_pluginInterface) .Invoke(guid2, _objectManager.Objects[_gameObjectIndex] as Character); + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidOnceToCharacter); + ImGui.TableNextColumn(); + if (ImGui.Button("Apply Once##ByGuidCharacter") && Guid.TryParse(_designIdentifier, out var guid2Once)) + GlamourerIpc.ApplyByGuidOnceToCharacterSubscriber(_pluginInterface) + .Invoke(guid2Once, _objectManager.Objects[_gameObjectIndex] as Character); + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelUnlock); ImGui.TableNextColumn(); @@ -149,6 +173,17 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag ImGui.TextUnformatted(_setItemEc.ToString()); } + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItemOnce); + ImGui.TableNextColumn(); + if (ImGui.Button("Set Once##SetItem")) + _setItemOnceEc = (GlamourerIpc.GlamourerErrorCode)GlamourerIpc.SetItemOnceSubscriber(_pluginInterface) + .Invoke(_objectManager.Objects[_gameObjectIndex] as Character, (byte)_slot, _customItemId.Id, _stainId.Id, 1337); + if (_setItemOnceEc != GlamourerIpc.GlamourerErrorCode.Success) + { + ImGui.SameLine(); + ImGui.TextUnformatted(_setItemOnceEc.ToString()); + } + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItemByActorName); ImGui.TableNextColumn(); if (ImGui.Button("Set##SetItemByActorName")) @@ -159,6 +194,17 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag ImGui.SameLine(); ImGui.TextUnformatted(_setItemByActorNameEc.ToString()); } + + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItemOnceByActorName); + ImGui.TableNextColumn(); + if (ImGui.Button("Set Once##SetItemByActorName")) + _setItemOnceByActorNameEc = (GlamourerIpc.GlamourerErrorCode)GlamourerIpc.SetItemOnceByActorNameSubscriber(_pluginInterface) + .Invoke(_gameObjectName, (byte)_slot, _customItemId.Id, _stainId.Id, 1337); + if (_setItemOnceByActorNameEc != GlamourerIpc.GlamourerErrorCode.Success) + { + ImGui.SameLine(); + ImGui.TextUnformatted(_setItemOnceByActorNameEc.ToString()); + } } private void DrawItemInput() diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs index 00a23cc..ecbf0e7 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs @@ -95,9 +95,13 @@ public class DesignDetailTab Glamourer.Messager.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}", NotificationType.Warning); } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.SetClipboardText(identifier); } - ImGuiUtil.HoverTooltip($"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice."); + ImGuiUtil.HoverTooltip( + $"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.\n\nRight-Click to copy identifier to clipboard."); ImGuiUtil.DrawFrameColumn("Full Selector Path"); ImGui.TableNextColumn(); @@ -131,9 +135,10 @@ public class DesignDetailTab colorName = _colorCombo.CurrentSelection is DesignColors.AutomaticName ? string.Empty : _colorCombo.CurrentSelection; _manager.ChangeColor(_selector.Selected!, colorName); } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) _manager.ChangeColor(_selector.Selected!, string.Empty); - + if (_colors.TryGetValue(_selector.Selected!.Color, out var currentColor)) { ImGui.SameLine(); diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index e8435ce..ef75245 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -89,6 +89,11 @@ public class SettingsTab( Checkbox("Enable Advanced Customization Options", "Enable the display and editing of advanced customization options like arbitrary colors.", config.UseAdvancedParameters, paletteChecker.SetAdvancedParameters); + Checkbox("Always Apply Associated Mods", + "Whenever a design is applied to a character (including via automation), Glamourer will try to apply its associated mod settings to the collection currently associated with that character, if it is available.\n\n" + + "Glamourer will NOT revert these applied settings automatically. This may mess up your collection and configuration.\n\n" + + "If you enable this setting, you are aware that any resulting misconfiguration is your own fault.", + config.AlwaysApplyAssociatedMods, v => config.AlwaysApplyAssociatedMods = v); PaletteImportButton(); ImGui.NewLine(); } diff --git a/Glamourer/Interop/Material/PrepareColorSet.cs b/Glamourer/Interop/Material/PrepareColorSet.cs index 7cfbf6c..4230fc1 100644 --- a/Glamourer/Interop/Material/PrepareColorSet.cs +++ b/Glamourer/Interop/Material/PrepareColorSet.cs @@ -131,19 +131,16 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable } else { - switch (source) + switch (source.Base()) { case StateSource.Manual: - case StateSource.Pending: state.Materials.RemoveValue(idx); --i; break; - case StateSource.Ipc: case StateSource.Fixed: idx.DataIndex.SetValue(ref row, model); state.Materials.UpdateValue(idx, new MaterialValueState(newGame, model, source), out _); break; - } } } diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index 7222a4b..e9b74fd 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -118,8 +118,7 @@ public class StateApplier( // If the source is not IPC we do not want to apply restrictions. var data = GetData(state); if (apply) - ChangeArmor(data, slot, state.ModelData.Armor(slot), state.Sources[slot, false] is not StateSource.Ipc, - state.ModelData.IsHatVisible()); + ChangeArmor(data, slot, state.ModelData.Armor(slot), !state.Sources[slot, false].IsIpc(), state.ModelData.IsHatVisible()); return data; } @@ -267,7 +266,7 @@ public class StateApplier( actor.Model.ApplyParameterData(flags, values); } - /// + /// public ActorData ChangeParameters(ActorState state, CustomizeParameterFlag flags, bool apply) { var data = GetData(state); @@ -294,10 +293,7 @@ public class StateApplier( { ChangeCustomize(actors, state.ModelData.Customize); foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - ChangeArmor(actors, slot, state.ModelData.Armor(slot), state.Sources[slot, false] is not StateSource.Ipc, - state.ModelData.IsHatVisible()); - } + ChangeArmor(actors, slot, state.ModelData.Armor(slot), !state.Sources[slot, false].IsIpc(), state.ModelData.IsHatVisible()); var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors; ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 2b1337e..48b74ab 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -31,7 +31,7 @@ public class StateEditor( if (!Editor.ChangeModelId(state, modelId, customize, equipData, source, out var old, key)) return; - var actors = Applier.ForceRedraw(state, source is StateSource.Manual or StateSource.Ipc); + var actors = Applier.ForceRedraw(state, source.RequiresChange()); Glamourer.Log.Verbose( $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChanged.Type.Model, source, state, actors, (old, modelId)); @@ -44,7 +44,7 @@ public class StateEditor( if (!Editor.ChangeCustomize(state, idx, value, settings.Source, out var old, settings.Key)) return; - var actors = Applier.ChangeCustomize(state, settings.Source is StateSource.Manual or StateSource.Ipc); + var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChanged.Type.Customize, settings.Source, state, actors, (old, value, idx)); @@ -57,7 +57,7 @@ public class StateEditor( if (!Editor.ChangeHumanCustomize(state, customizeInput, apply, _ => settings.Source, out var old, out var applied, settings.Key)) return; - var actors = Applier.ChangeCustomize(state, settings.Source is StateSource.Manual or StateSource.Ipc); + var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChanged.Type.EntireCustomize, settings.Source, state, actors, (old, applied)); @@ -72,8 +72,8 @@ public class StateEditor( var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon; var actors = type is StateChanged.Type.Equip - ? Applier.ChangeArmor(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc) - : Applier.ChangeWeapon(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc, + ? Applier.ChangeArmor(state, slot, settings.Source.RequiresChange()) + : Applier.ChangeWeapon(state, slot, settings.Source.RequiresChange(), item.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); if (slot is EquipSlot.MainHand) @@ -105,8 +105,8 @@ public class StateEditor( var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon; var actors = type is StateChanged.Type.Equip - ? Applier.ChangeArmor(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc) - : Applier.ChangeWeapon(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc, + ? Applier.ChangeArmor(state, slot, settings.Source.RequiresChange()) + : Applier.ChangeWeapon(state, slot, settings.Source.RequiresChange(), item!.Value.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); if (slot is EquipSlot.MainHand) @@ -125,7 +125,7 @@ public class StateEditor( if (!Editor.ChangeStain(state, slot, stain, settings.Source, out var old, settings.Key)) return; - var actors = Applier.ChangeStain(state, slot, settings.Source is StateSource.Manual or StateSource.Ipc); + var actors = Applier.ChangeStain(state, slot, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old.Id} to {stain.Id}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChanged.Type.Stain, settings.Source, state, actors, (old, stain, slot)); @@ -138,7 +138,7 @@ public class StateEditor( if (!Editor.ChangeCrest(state, slot, crest, settings.Source, out var old, settings.Key)) return; - var actors = Applier.ChangeCrests(state, settings.Source is StateSource.Manual or StateSource.Ipc); + var actors = Applier.ChangeCrests(state, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {slot.ToLabel()} crest in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChanged.Type.Crest, settings.Source, state, actors, (old, crest, slot)); @@ -158,7 +158,7 @@ public class StateEditor( return; var @new = state.ModelData.Parameters[flag]; - var actors = Applier.ChangeParameters(state, flag, settings.Source is StateSource.Manual or StateSource.Ipc); + var actors = Applier.ChangeParameters(state, flag, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {flag} crest in state {state.Identifier.Incognito(null)} from {old} to {@new}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChanged.Type.Parameter, settings.Source, state, actors, (old, @new, flag)); @@ -171,7 +171,7 @@ public class StateEditor( if (!Editor.ChangeMetaState(state, index, value, settings.Source, out var old, settings.Key)) return; - var actors = Applier.ChangeMetaState(state, index, settings.Source is StateSource.Manual or StateSource.Ipc); + var actors = Applier.ChangeMetaState(state, index, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set Head Gear Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChanged.Type.Other, settings.Source, state, actors, (old, value, MetaIndex.HatState)); @@ -191,7 +191,7 @@ public class StateEditor( { foreach (var slot in CrestExtensions.AllRelevantSet.Where(mergedDesign.Design.DoApplyCrest)) { - if (!settings.RespectManual || state.Sources[slot] is not StateSource.Manual) + if (!settings.RespectManual || !state.Sources[slot].IsManual()) Editor.ChangeCrest(state, slot, mergedDesign.Design.DesignData.Crest(slot), Source(slot), out _, settings.Key); } @@ -201,7 +201,7 @@ public class StateEditor( customizeFlags |= CustomizeFlag.Race; Func applyWhich = settings.RespectManual - ? i => customizeFlags.HasFlag(i.ToFlag()) && state.Sources[i] is not StateSource.Manual + ? i => customizeFlags.HasFlag(i.ToFlag()) && !state.Sources[i].IsManual() : i => customizeFlags.HasFlag(i.ToFlag()); if (Editor.ChangeHumanCustomize(state, mergedDesign.Design.DesignData.Customize, applyWhich, i => Source(i), out _, out var changed, @@ -210,12 +210,10 @@ public class StateEditor( foreach (var parameter in mergedDesign.Design.ApplyParameters.Iterate()) { - if (settings.RespectManual && state.Sources[parameter] is StateSource.Manual or StateSource.Pending) + if (settings.RespectManual && state.Sources[parameter].IsManual()) continue; - var source = Source(parameter); - if (source is StateSource.Manual) - source = StateSource.Pending; + var source = Source(parameter).SetPending(); Editor.ChangeParameter(state, parameter, mergedDesign.Design.DesignData.Parameters[parameter], source, out _, settings.Key); } @@ -228,12 +226,12 @@ public class StateEditor( foreach (var slot in EquipSlotExtensions.EqdpSlots) { if (mergedDesign.Design.DoApplyEquip(slot)) - if (!settings.RespectManual || state.Sources[slot, false] is not StateSource.Manual) + if (!settings.RespectManual || !state.Sources[slot, false].IsManual()) Editor.ChangeItem(state, slot, mergedDesign.Design.DesignData.Item(slot), Source(slot.ToState()), out _, settings.Key); if (mergedDesign.Design.DoApplyStain(slot)) - if (!settings.RespectManual || state.Sources[slot, true] is not StateSource.Manual) + if (!settings.RespectManual || !state.Sources[slot, true].IsManual()) Editor.ChangeStain(state, slot, mergedDesign.Design.DesignData.Stain(slot), Source(slot.ToState(true)), out _, settings.Key); } @@ -241,14 +239,14 @@ public class StateEditor( foreach (var weaponSlot in EquipSlotExtensions.WeaponSlots) { if (mergedDesign.Design.DoApplyStain(weaponSlot)) - if (!settings.RespectManual || state.Sources[weaponSlot, true] is not StateSource.Manual) + if (!settings.RespectManual || !state.Sources[weaponSlot, true].IsManual()) Editor.ChangeStain(state, weaponSlot, mergedDesign.Design.DesignData.Stain(weaponSlot), Source(weaponSlot.ToState(true)), out _, settings.Key); if (!mergedDesign.Design.DoApplyEquip(weaponSlot)) continue; - if (settings.RespectManual && state.Sources[weaponSlot, false] is StateSource.Manual) + if (settings.RespectManual && !state.Sources[weaponSlot, false].IsManual()) continue; var currentType = state.ModelData.Item(weaponSlot).Type; @@ -268,12 +266,12 @@ public class StateEditor( foreach (var meta in MetaExtensions.AllRelevant) { - if (!settings.RespectManual || state.Sources[meta] is not StateSource.Manual) + if (!settings.RespectManual || !state.Sources[meta].IsManual()) Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key); } } - var actors = settings.Source is StateSource.Manual or StateSource.Ipc + var actors = settings.Source.RequiresChange() ? Applier.ApplyAll(state, requiresRedraw, false) : ActorData.Invalid; @@ -311,7 +309,7 @@ public class StateEditor( /// Apply offhand item and potentially gauntlets if configured. private void ApplyMainhandPeriphery(ActorState state, EquipItem? newMainhand, ApplySettings settings) { - if (!Config.ChangeEntireItem || settings.Source is not StateSource.Manual) + if (!Config.ChangeEntireItem || !settings.Source.IsManual()) return; var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand); diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 1833660..23e0f3c 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -171,7 +171,7 @@ public class StateListener : IDisposable var set = _customizations.Manager.GetSet(model.Clan, model.Gender); foreach (var index in CustomizationExtensions.AllBasic) { - if (state.Sources[index] is not StateSource.Fixed) + if (!state.Sources[index].IsFixed()) { var newValue = customize[index]; var oldValue = model[index]; @@ -214,7 +214,7 @@ public class StateListener : IDisposable && _manager.TryGetValue(identifier, out var state)) { HandleEquipSlot(actor, state, slot, ref armor); - locked = state.Sources[slot, false] is StateSource.Ipc; + locked = state.Sources[slot, false] is StateSource.IpcFixed; } _funModule.ApplyFunToSlot(actor, ref armor, slot); @@ -241,7 +241,7 @@ public class StateListener : IDisposable continue; var changed = changedItem.Weapon(stain); - if (current.Value == changed.Value && state.Sources[slot, false] is not StateSource.Fixed and not StateSource.Ipc) + if (current.Value == changed.Value && !state.Sources[slot, false].IsFixed()) { _manager.ChangeItem(state, slot, currentItem, ApplySettings.Game); _manager.ChangeStain(state, slot, current.Stain, ApplySettings.Game); @@ -252,7 +252,7 @@ public class StateListener : IDisposable _applier.ChangeWeapon(objects, slot, currentItem, stain); break; default: - _applier.ChangeArmor(objects, slot, current.ToArmor(), state.Sources[slot, false] is not StateSource.Ipc, + _applier.ChangeArmor(objects, slot, current.ToArmor(), !state.Sources[slot, false].IsFixed(), state.ModelData.IsHatVisible()); break; } @@ -278,20 +278,19 @@ public class StateListener : IDisposable || !_manager.TryGetValue(identifier, out var state)) return; - ref var actorWeapon = ref weapon; - var baseType = state.BaseData.Item(slot).Type; - var apply = false; - switch (UpdateBaseData(actor, state, slot, actorWeapon)) + var baseType = state.BaseData.Item(slot).Type; + var apply = false; + switch (UpdateBaseData(actor, state, slot, weapon)) { // Do nothing. But this usually can not happen because the hooked function also writes to game objects later. case UpdateState.Transformed: break; case UpdateState.Change: - if (state.Sources[slot, false] is not StateSource.Fixed and not StateSource.Ipc) + if (!state.Sources[slot, false].IsFixed()) _manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game); else apply = true; - if (state.Sources[slot, true] is not StateSource.Fixed and not StateSource.Ipc) + if (!state.Sources[slot, true].IsFixed()) _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); else apply = true; @@ -306,9 +305,9 @@ public class StateListener : IDisposable // Only allow overwriting identical weapons var newWeapon = state.ModelData.Weapon(slot); if (baseType is FullEquipType.Unknown || baseType == state.ModelData.Item(slot).Type || _gPose.InGPose && actor.IsGPoseOrCutscene) - actorWeapon = newWeapon; - else if (actorWeapon.Skeleton.Id != 0) - actorWeapon = actorWeapon.With(newWeapon.Stain); + weapon = newWeapon; + else if (weapon.Skeleton.Id != 0) + weapon = weapon.With(newWeapon.Stain); } // Fist Weapon Offhand hack. @@ -385,12 +384,12 @@ public class StateListener : IDisposable // Update model state if not on fixed design. case UpdateState.Change: var apply = false; - if (state.Sources[slot, false] is not StateSource.Fixed and not StateSource.Ipc) + if (!state.Sources[slot, false].IsFixed()) _manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game); else apply = true; - if (state.Sources[slot, true] is not StateSource.Fixed and not StateSource.Ipc) + if (!state.Sources[slot, true].IsFixed()) _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); else apply = true; @@ -419,7 +418,7 @@ public class StateListener : IDisposable switch (UpdateBaseCrest(actor, state, slot, value)) { case UpdateState.Change: - if (state.Sources[slot] is not StateSource.Fixed and not StateSource.Ipc) + if (!state.Sources[slot].IsFixed()) _manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), ApplySettings.Game); else value = state.ModelData.Crest(slot); @@ -565,7 +564,7 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state.Sources[MetaIndex.VisorState] is StateSource.Fixed or StateSource.Ipc) + if (!state.Sources[MetaIndex.VisorState].IsFixed()) value = state.ModelData.IsVisorToggled(); else _manager.ChangeMetaState(state, MetaIndex.VisorState, value, ApplySettings.Game); @@ -598,7 +597,7 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state.Sources[MetaIndex.HatState] is StateSource.Fixed or StateSource.Ipc) + if (!state.Sources[MetaIndex.HatState].IsFixed()) value = state.ModelData.IsHatVisible(); else _manager.ChangeMetaState(state, MetaIndex.HatState, value, ApplySettings.Game); @@ -631,7 +630,7 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state.Sources[MetaIndex.WeaponState] is StateSource.Fixed or StateSource.Ipc) + if (!state.Sources[MetaIndex.WeaponState].IsFixed()) value = state.ModelData.IsWeaponVisible(); else _manager.ChangeMetaState(state, MetaIndex.WeaponState, value, ApplySettings.Game); @@ -700,8 +699,8 @@ public class StateListener : IDisposable return; var data = new ActorData(gameObject, _creatingIdentifier.ToName()); - _applier.ChangeMetaState(data, MetaIndex.HatState, _creatingState.ModelData.IsHatVisible()); - _applier.ChangeMetaState(data, MetaIndex.Wetness, _creatingState.ModelData.IsWet()); + _applier.ChangeMetaState(data, MetaIndex.HatState, _creatingState.ModelData.IsHatVisible()); + _applier.ChangeMetaState(data, MetaIndex.Wetness, _creatingState.ModelData.IsWet()); _applier.ChangeMetaState(data, MetaIndex.WeaponState, _creatingState.ModelData.IsWeaponVisible()); ApplyParameters(_creatingState, drawObject); @@ -745,12 +744,18 @@ public class StateListener : IDisposable else if (_config.UseAdvancedParameters) model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; + case StateSource.IpcManual: + if (state.BaseData.Parameters.Set(flag, newValue)) + _manager.ChangeCustomizeParameter(state, flag, newValue, ApplySettings.Game); + else + model.ApplySingleParameterData(flag, state.ModelData.Parameters); + break; case StateSource.Fixed: state.BaseData.Parameters.Set(flag, newValue); if (_config.UseAdvancedParameters) model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; - case StateSource.Ipc: + case StateSource.IpcFixed: state.BaseData.Parameters.Set(flag, newValue); model.ApplySingleParameterData(flag, state.ModelData.Parameters); break; diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index c6658c5..e6ed657 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -243,7 +243,7 @@ public sealed class StateManager( state.Materials.Clear(); var actors = ActorData.Invalid; - if (source is StateSource.Manual or StateSource.Ipc) + if (source is not StateSource.Game) actors = Applier.ApplyAll(state, redraw, true); Glamourer.Log.Verbose( @@ -262,7 +262,7 @@ public sealed class StateManager( state.Sources[flag] = StateSource.Game; var actors = ActorData.Invalid; - if (source is StateSource.Manual or StateSource.Ipc) + if (source is not StateSource.Game) actors = Applier.ChangeParameters(state, CustomizeParameterExtensions.All, true); Glamourer.Log.Verbose( $"Reset advanced customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); @@ -316,28 +316,10 @@ public sealed class StateManager( } } - if (state.Sources[MetaIndex.HatState] is StateSource.Fixed) + foreach (var meta in MetaExtensions.AllRelevant.Where(f => state.Sources[f] is StateSource.Fixed)) { - state.Sources[MetaIndex.HatState] = StateSource.Game; - state.ModelData.SetHatVisible(state.BaseData.IsHatVisible()); - } - - if (state.Sources[MetaIndex.VisorState] is StateSource.Fixed) - { - state.Sources[MetaIndex.VisorState] = StateSource.Game; - state.ModelData.SetVisor(state.BaseData.IsVisorToggled()); - } - - if (state.Sources[MetaIndex.WeaponState] is StateSource.Fixed) - { - state.Sources[MetaIndex.WeaponState] = StateSource.Game; - state.ModelData.SetWeaponVisible(state.BaseData.IsWeaponVisible()); - } - - if (state.Sources[MetaIndex.Wetness] is StateSource.Fixed) - { - state.Sources[MetaIndex.Wetness] = StateSource.Game; - state.ModelData.SetIsWet(state.BaseData.IsWet()); + state.Sources[meta] = StateSource.Game; + state.ModelData.SetMeta(meta, state.BaseData.GetMeta(meta)); } } diff --git a/Glamourer/State/StateSource.cs b/Glamourer/State/StateSource.cs index 1d66a46..d489814 100644 --- a/Glamourer/State/StateSource.cs +++ b/Glamourer/State/StateSource.cs @@ -7,12 +7,48 @@ public enum StateSource : byte Game, Manual, Fixed, - Ipc, + IpcFixed, + IpcManual, // Only used for CustomizeParameters. Pending, } +public static class StateSourceExtensions +{ + public static StateSource Base(this StateSource source) + => source switch + { + StateSource.Manual or StateSource.IpcManual or StateSource.Pending => StateSource.Manual, + StateSource.Fixed or StateSource.IpcFixed => StateSource.Fixed, + _ => StateSource.Game, + }; + + public static bool IsGame(this StateSource source) + => source.Base() is StateSource.Game; + + public static bool IsManual(this StateSource source) + => source.Base() is StateSource.Manual; + + public static bool IsFixed(this StateSource source) + => source.Base() is StateSource.Fixed; + + public static StateSource SetPending(this StateSource source) + => source is StateSource.Manual ? StateSource.Pending : source; + + public static bool RequiresChange(this StateSource source) + => source switch + { + StateSource.Manual => true, + StateSource.IpcFixed => true, + StateSource.IpcManual => true, + _ => false, + }; + + public static bool IsIpc(this StateSource source) + => source is StateSource.IpcManual or StateSource.IpcFixed; +} + public unsafe struct StateSources { public const int Size = (StateIndex.Size + 1) / 2; @@ -59,14 +95,16 @@ public unsafe struct StateSources case (byte)StateSource.Game | ((byte)StateSource.Fixed << 4): case (byte)StateSource.Manual | ((byte)StateSource.Fixed << 4): - case (byte)StateSource.Ipc | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.IpcFixed | ((byte)StateSource.Fixed << 4): case (byte)StateSource.Pending | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.IpcManual | ((byte)StateSource.Fixed << 4): _data[i] = (byte)((value & 0x0F) | ((byte)StateSource.Manual << 4)); break; case (byte)StateSource.Fixed: case ((byte)StateSource.Manual << 4) | (byte)StateSource.Fixed: - case ((byte)StateSource.Ipc << 4) | (byte)StateSource.Fixed: + case ((byte)StateSource.IpcFixed << 4) | (byte)StateSource.Fixed: case ((byte)StateSource.Pending << 4) | (byte)StateSource.Fixed: + case ((byte)StateSource.IpcManual << 4) | (byte)StateSource.Fixed: _data[i] = (byte)((value & 0xF0) | (byte)StateSource.Manual); break; } From d10043a69a01e45d84b03b19d4e1b1a5b5ef774c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jan 2024 18:30:51 +0100 Subject: [PATCH 10/17] Add toggle for always applying mod associations. --- Glamourer/Automation/AutoDesignApplier.cs | 2 +- Glamourer/Gui/Tabs/SettingsTab.cs | 2 +- .../Interop/Penumbra/ModSettingApplier.cs | 67 +++++++++++++++++++ Glamourer/Services/CommandService.cs | 21 ++---- Glamourer/State/StateEditor.cs | 7 +- Glamourer/State/StateManager.cs | 6 +- 6 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 Glamourer/Interop/Penumbra/ModSettingApplier.cs diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index dd327b3..65116db 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -264,7 +264,7 @@ public sealed class AutoDesignApplier : IDisposable var mergedDesign = _designMerger.Merge( set.Designs.Where(d => d.IsActive(actor)).SelectMany(d => d.Design?.AllLinks.Select(l => (l.Design, l.Flags & d.Type)) ?? [(d.Design, d.Type)]), - state.ModelData, true, false); + state.ModelData, true, _config.AlwaysApplyAssociatedMods); _state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false)); } diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index ef75245..11490df 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -89,12 +89,12 @@ public class SettingsTab( Checkbox("Enable Advanced Customization Options", "Enable the display and editing of advanced customization options like arbitrary colors.", config.UseAdvancedParameters, paletteChecker.SetAdvancedParameters); + PaletteImportButton(); Checkbox("Always Apply Associated Mods", "Whenever a design is applied to a character (including via automation), Glamourer will try to apply its associated mod settings to the collection currently associated with that character, if it is available.\n\n" + "Glamourer will NOT revert these applied settings automatically. This may mess up your collection and configuration.\n\n" + "If you enable this setting, you are aware that any resulting misconfiguration is your own fault.", config.AlwaysApplyAssociatedMods, v => config.AlwaysApplyAssociatedMods = v); - PaletteImportButton(); ImGui.NewLine(); } diff --git a/Glamourer/Interop/Penumbra/ModSettingApplier.cs b/Glamourer/Interop/Penumbra/ModSettingApplier.cs new file mode 100644 index 0000000..01ea4d7 --- /dev/null +++ b/Glamourer/Interop/Penumbra/ModSettingApplier.cs @@ -0,0 +1,67 @@ +using Glamourer.Designs.Links; +using Glamourer.Interop.Structs; +using Glamourer.State; +using OtterGui.Services; + +namespace Glamourer.Interop.Penumbra; + +public class ModSettingApplier(PenumbraService penumbra, Configuration config, ObjectManager objects) : IService +{ + public void HandleStateApplication(ActorState state, MergedDesign design) + { + if (!config.AlwaysApplyAssociatedMods || design.AssociatedMods.Count == 0) + return; + + objects.Update(); + if (!objects.TryGetValue(state.Identifier, out var data)) + { + Glamourer.Log.Verbose( + $"[Mod Applier] No mod settings applied because no actor for {state.Identifier} could be found to associate collection."); + return; + } + + var collections = new HashSet(); + + foreach (var actor in data.Objects) + { + var collection = penumbra.GetActorCollection(actor); + if (collection.Length == 0) + { + Glamourer.Log.Verbose($"[Mod Applier] Could not obtain associated collection for {actor.Utf8Name}."); + continue; + } + + if (!collections.Add(collection)) + continue; + + foreach (var (mod, setting) in design.AssociatedMods) + { + var message = penumbra.SetMod(mod, setting, collection); + if (message.Length > 0) + Glamourer.Log.Verbose($"[Mod Applier] Error applying mod settings: {message}"); + else + Glamourer.Log.Verbose($"[Mod Applier] Set mod settings for {mod.DirectoryName} in {collection}."); + } + } + } + + public (List Messages, int Applied, string Collection) ApplyModSettings(IReadOnlyDictionary settings, Actor actor) + { + var collection = penumbra.GetActorCollection(actor); + if (collection.Length <= 0) + return ([$"Could not obtain associated collection for {actor.Utf8Name}."], 0, string.Empty); + + var messages = new List(); + var appliedMods = 0; + foreach (var (mod, setting) in settings) + { + var message = penumbra.SetMod(mod, setting, collection); + if (message.Length > 0) + messages.Add($"Error applying mod settings: {message}"); + else + ++appliedMods; + } + + return (messages, appliedMods, collection); + } +} diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 895ad2f..2f3e938 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -33,11 +33,11 @@ public class CommandService : IDisposable private readonly DesignConverter _converter; private readonly DesignFileSystem _designFileSystem; private readonly Configuration _config; - private readonly PenumbraService _penumbra; + private readonly ModSettingApplier _modApplier; public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorManager actors, ObjectManager objects, AutoDesignApplier autoDesignApplier, StateManager stateManager, DesignManager designManager, DesignConverter converter, - DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config, PenumbraService penumbra) + DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config, ModSettingApplier modApplier) { _commands = commands; _mainWindow = mainWindow; @@ -51,7 +51,7 @@ public class CommandService : IDisposable _designFileSystem = designFileSystem; _autoDesignManager = autoDesignManager; _config = config; - _penumbra = penumbra; + _modApplier = modApplier; _commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." }); _commands.AddHandler(ApplyCommandString, @@ -440,19 +440,10 @@ public class CommandService : IDisposable if (!applyMods || design is not Design d) return; - var collection = _penumbra.GetActorCollection(actor); - if (collection.Length <= 0) - return; + var (messages, appliedMods, collection) = _modApplier.ApplyModSettings(d.AssociatedMods, actor); - var appliedMods = 0; - foreach (var (mod, setting) in d.AssociatedMods) - { - var message = _penumbra.SetMod(mod, setting, collection); - if (message.Length > 0) - Glamourer.Messager.Chat.Print($"Error applying mod settings: {message}"); - else - ++appliedMods; - } + foreach (var message in messages) + Glamourer.Messager.Chat.Print($"Error applying mod settings: {message}"); if (appliedMods > 0) Glamourer.Messager.Chat.Print($"Applied {appliedMods} mod settings to {collection}."); diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 48b74ab..a6606e6 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -2,6 +2,7 @@ using Glamourer.Designs; using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; +using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; using Penumbra.GameData.Enums; @@ -16,7 +17,8 @@ public class StateEditor( JobChangeState jobChange, Configuration config, ItemManager items, - DesignMerger merger) : IDesignEditor + DesignMerger merger, + ModSettingApplier modApplier) : IDesignEditor { protected readonly InternalStateEditor Editor = editor; protected readonly StateApplier Applier = applier; @@ -181,6 +183,7 @@ public class StateEditor( public void ApplyDesign(object data, MergedDesign mergedDesign, ApplySettings settings) { var state = (ActorState)data; + modApplier.HandleStateApplication(state, mergedDesign); if (!Editor.ChangeModelId(state, mergedDesign.Design.DesignData.ModelId, mergedDesign.Design.DesignData.Customize, mergedDesign.Design.GetDesignDataRef().GetEquipmentPtr(), settings.Source, out var oldModelId, settings.Key)) return; @@ -294,7 +297,7 @@ public class StateEditor( public void ApplyDesign(object data, DesignBase design, ApplySettings settings) { var merged = settings.MergeLinks && design is Design d - ? merger.Merge(d.AllLinks, ((ActorState)data).ModelData, false, false) + ? merger.Merge(d.AllLinks, ((ActorState)data).ModelData, false, Config.AlwaysApplyAssociatedMods) : new MergedDesign(design); ApplyDesign(data, merged, settings with diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index e6ed657..32d1237 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -4,6 +4,7 @@ using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; using Penumbra.GameData.Actors; @@ -23,8 +24,9 @@ public sealed class StateManager( IClientState _clientState, Configuration config, JobChangeState jobChange, - DesignMerger merger) - : StateEditor(editor, applier, @event, jobChange, config, items, merger), IReadOnlyDictionary + DesignMerger merger, + ModSettingApplier modApplier) + : StateEditor(editor, applier, @event, jobChange, config, items, merger, modApplier), IReadOnlyDictionary { private readonly Dictionary _states = []; From 5cdcb9288e0109a188c739adb46a6751dc9f1a6c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Feb 2024 13:51:38 +0100 Subject: [PATCH 11/17] Add NearEqual for vectors. --- Glamourer/Designs/ApplicationRules.cs | 2 +- Glamourer/GameData/CustomizeParameterValue.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Glamourer/Designs/ApplicationRules.cs b/Glamourer/Designs/ApplicationRules.cs index 1852c7e..aaaf1a7 100644 --- a/Glamourer/Designs/ApplicationRules.cs +++ b/Glamourer/Designs/ApplicationRules.cs @@ -75,7 +75,7 @@ public readonly struct ApplicationRules( { var modelValue = model.Parameters[flag]; var gameValue = game.Parameters[flag]; - if ((modelValue.InternalQuadruple - gameValue.InternalQuadruple).LengthSquared() < 1e-9f) + if (modelValue.NearEqual(gameValue)) baseFlags &= ~flag; } diff --git a/Glamourer/GameData/CustomizeParameterValue.cs b/Glamourer/GameData/CustomizeParameterValue.cs index 0e22d18..3bfdf99 100644 --- a/Glamourer/GameData/CustomizeParameterValue.cs +++ b/Glamourer/GameData/CustomizeParameterValue.cs @@ -48,3 +48,18 @@ public readonly struct CustomizeParameterValue public override string ToString() => _data.ToString(); } + +public static class VectorExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static bool NearEqual(this Vector3 lhs, Vector3 rhs, float eps = 1e-9f) + => (lhs - rhs).LengthSquared() < eps; + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static bool NearEqual(this Vector4 lhs, Vector4 rhs, float eps = 1e-9f) + => (lhs - rhs).LengthSquared() < eps; + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static bool NearEqual(this CustomizeParameterValue lhs, CustomizeParameterValue rhs, float eps = 1e-9f) + => NearEqual(lhs.InternalQuadruple, rhs.InternalQuadruple, eps); +} From fb7aac5228bad14e3f2a938598b27c13e4c38a46 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Feb 2024 13:52:20 +0100 Subject: [PATCH 12/17] Add state editing and tracking. --- Glamourer/Events/StateChanged.cs | 3 + Glamourer/Gui/Materials/MaterialDrawer.cs | 97 ++++++---- Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 34 ---- Glamourer/Interop/Material/MaterialManager.cs | 148 ++++++++++++++++ .../Interop/Material/MaterialValueIndex.cs | 26 ++- .../Interop/Material/MaterialValueManager.cs | 59 ++++--- Glamourer/Interop/Material/PrepareColorSet.cs | 165 +++--------------- Glamourer/State/InternalStateEditor.cs | 34 ++++ Glamourer/State/StateApplier.cs | 36 ++++ Glamourer/State/StateEditor.cs | 16 +- 10 files changed, 383 insertions(+), 235 deletions(-) create mode 100644 Glamourer/Interop/Material/MaterialManager.cs diff --git a/Glamourer/Events/StateChanged.cs b/Glamourer/Events/StateChanged.cs index 47b5d20..e5e0cb5 100644 --- a/Glamourer/Events/StateChanged.cs +++ b/Glamourer/Events/StateChanged.cs @@ -42,6 +42,9 @@ namespace Glamourer.Events /// A characters saved state had its customize parameter changed. Data is the old value, the new value and the type [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. Parameter, + /// A characters saved state had a material color table value changed. Data is the old value, the new value and the index [(Vector3, Vector3, MaterialValueIndex)]. + MaterialValue, + /// A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] Design, diff --git a/Glamourer/Gui/Materials/MaterialDrawer.cs b/Glamourer/Gui/Materials/MaterialDrawer.cs index 1114911..e4a612d 100644 --- a/Glamourer/Gui/Materials/MaterialDrawer.cs +++ b/Glamourer/Gui/Materials/MaterialDrawer.cs @@ -1,16 +1,18 @@ -using Dalamud.Interface.Utility; +using Dalamud.Interface; +using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Designs; using Glamourer.Interop.Material; using Glamourer.Interop.Structs; +using Glamourer.State; using ImGuiNET; using OtterGui.Services; using Penumbra.GameData.Files; namespace Glamourer.Gui.Materials; -public unsafe class MaterialDrawer : IService +public unsafe class MaterialDrawer(StateManager _stateManager) : IService { private static readonly IReadOnlyList Types = [ @@ -19,9 +21,11 @@ public unsafe class MaterialDrawer : IService MaterialValueIndex.DrawObjectType.Offhand, ]; + private ActorState? _state; + public void DrawPanel(Actor actor) { - if (!actor.IsCharacter) + if (!actor.IsCharacter || !_stateManager.GetOrCreate(actor, out _state)) return; foreach (var type in Types) @@ -64,11 +68,11 @@ public unsafe class MaterialDrawer : IService if (!DirectXTextureHelper.TryGetColorTable(*texture, out var table)) continue; - DrawMaterial(ref table, texture, index); + DrawMaterial(ref table, index); } } - private void DrawMaterial(ref MtrlFile.ColorTable table, Texture** texture, MaterialValueIndex sourceIndex) + private void DrawMaterial(ref MtrlFile.ColorTable table, MaterialValueIndex sourceIndex) { using var tree = ImRaii.TreeNode($"Material {sourceIndex.MaterialIndex + 1}"); if (!tree) @@ -76,55 +80,78 @@ public unsafe class MaterialDrawer : IService for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i) { - var index = sourceIndex with { RowIndex = i }; + var index = sourceIndex with { RowIndex = i }; ref var row = ref table[i]; - DrawRow(ref table, ref row, texture, index); + DrawRow(ref row, index); } } - private void DrawRow(ref MtrlFile.ColorTable table, ref MtrlFile.ColorTable.Row row, Texture** texture, MaterialValueIndex sourceIndex) + private void DrawRow(ref MtrlFile.ColorTable.Row row, MaterialValueIndex sourceIndex) { - using var id = ImRaii.PushId(sourceIndex.RowIndex); - var diffuse = row.Diffuse; - var specular = row.Specular; - var emissive = row.Emissive; - var glossStrength = row.GlossStrength; - var specularStrength = row.SpecularStrength; - if (ImGui.ColorEdit3("Diffuse", ref diffuse, ImGuiColorEditFlags.NoInputs)) + var r = _state!.Materials.GetValues( + MaterialValueIndex.Min(sourceIndex.DrawObject, sourceIndex.SlotIndex, sourceIndex.MaterialIndex, sourceIndex.RowIndex), + MaterialValueIndex.Max(sourceIndex.DrawObject, sourceIndex.SlotIndex, sourceIndex.MaterialIndex, sourceIndex.RowIndex)); + + var highlightColor = ColorId.FavoriteStarOn.Value(); + + using var id = ImRaii.PushId(sourceIndex.RowIndex); + var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Diffuse }; + var (diffuse, diffuseGame, changed) = MaterialValueManager.GetSpecific(r, index, out var d) + ? (d.Model, d.Game, true) + : (row.Diffuse, row.Diffuse, false); + using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) { - var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Diffuse }; - row.Diffuse = diffuse; - MaterialService.ReplaceColorTable(texture, table); + if (ImGui.ColorEdit3("Diffuse", ref diffuse, ImGuiColorEditFlags.NoInputs)) + _stateManager.ChangeMaterialValue(_state!, index, diffuse, diffuseGame, ApplySettings.Manual); } + + + index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Specular }; + (var specular, var specularGame, changed) = MaterialValueManager.GetSpecific(r, index, out var s) + ? (s.Model, s.Game, true) + : (row.Specular, row.Specular, false); ImGui.SameLine(); - if (ImGui.ColorEdit3("Specular", ref specular, ImGuiColorEditFlags.NoInputs)) + using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) { - var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Specular }; - row.Specular = specular; - MaterialService.ReplaceColorTable(texture, table); + if (ImGui.ColorEdit3("Specular", ref specular, ImGuiColorEditFlags.NoInputs)) + _stateManager.ChangeMaterialValue(_state!, index, specular, specularGame, ApplySettings.Manual); } + + index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Emissive }; + (var emissive, var emissiveGame, changed) = MaterialValueManager.GetSpecific(r, index, out var e) + ? (e.Model, e.Game, true) + : (row.Emissive, row.Emissive, false); ImGui.SameLine(); - if (ImGui.ColorEdit3("Emissive", ref emissive, ImGuiColorEditFlags.NoInputs)) + using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) { - var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Emissive }; - row.Emissive = emissive; - MaterialService.ReplaceColorTable(texture, table); + if (ImGui.ColorEdit3("Emissive", ref emissive, ImGuiColorEditFlags.NoInputs)) + _stateManager.ChangeMaterialValue(_state!, index, emissive, emissiveGame, ApplySettings.Manual); } + + index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.GlossStrength }; + (var glossStrength, var glossStrengthGame, changed) = MaterialValueManager.GetSpecific(r, index, out var g) + ? (g.Model.X, g.Game.X, true) + : (row.GlossStrength, row.GlossStrength, false); ImGui.SameLine(); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - if (ImGui.DragFloat("Gloss", ref glossStrength, 0.1f)) + using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) { - var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.GlossStrength }; - row.GlossStrength = glossStrength; - MaterialService.ReplaceColorTable(texture, table); + if (ImGui.DragFloat("Gloss", ref glossStrength, 0.1f)) + _stateManager.ChangeMaterialValue(_state!, index, new Vector3(glossStrength), new Vector3(glossStrengthGame), + ApplySettings.Manual); } + + index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.SpecularStrength }; + (var specularStrength, var specularStrengthGame, changed) = MaterialValueManager.GetSpecific(r, index, out var ss) + ? (ss.Model.X, ss.Game.X, true) + : (row.SpecularStrength, row.SpecularStrength, false); ImGui.SameLine(); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - if (ImGui.DragFloat("Specular Strength", ref specularStrength, 0.1f)) + using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) { - var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.SpecularStrength }; - row.SpecularStrength = specularStrength; - MaterialService.ReplaceColorTable(texture, table); + if (ImGui.DragFloat("Specular Strength", ref specularStrength, 0.1f)) + _stateManager.ChangeMaterialValue(_state!, index, new Vector3(specularStrength), new Vector3(specularStrengthGame), + ApplySettings.Manual); } } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 94f4b56..ff6b798 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -100,12 +100,6 @@ 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); @@ -126,34 +120,6 @@ public class ActorPanel( if (ImGui.CollapsingHeader("Material Shit")) _materialDrawer.DrawPanel(_actor); - 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 var current); - _test = current; - if (ImGui.ColorPicker3("TestPicker", ref _test) && _actor.Valid) - _state.Materials.AddOrUpdateValue(index, new MaterialValueState(current, _test, StateSource.Manual)); - - if (ImGui.ColorPicker3("TestPicker2", ref _test) && _actor.Valid) - _state.Materials.AddOrUpdateValue(index, new MaterialValueState(current, _test, StateSource.Fixed)); - - foreach (var value in _state.Materials.Values) - { - var id = MaterialValueIndex.FromKey(value.Key); - ImGui.TextUnformatted($"{id.DrawObject} {id.SlotIndex} {id.MaterialIndex} {id.RowIndex} {id.DataIndex} "); - ImGui.SameLine(0, 0); - var game = ImGui.ColorConvertFloat4ToU32(new Vector4(value.Value.Game, 1)); - ImGuiUtil.DrawTextButton(" ", Vector2.Zero, game); - ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X); - var model = ImGui.ColorConvertFloat4ToU32(new Vector4(value.Value.Model, 1)); - ImGuiUtil.DrawTextButton(" ", Vector2.Zero, model); - ImGui.SameLine(0, 0); - ImGui.TextUnformatted($" {value.Value.Source}"); - } - using var disabled = ImRaii.Disabled(transformationId != 0); if (_state.ModelData.IsHuman) DrawHumanPanel(); diff --git a/Glamourer/Interop/Material/MaterialManager.cs b/Glamourer/Interop/Material/MaterialManager.cs new file mode 100644 index 0000000..8cbe0c5 --- /dev/null +++ b/Glamourer/Interop/Material/MaterialManager.cs @@ -0,0 +1,148 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Glamourer.Designs; +using Glamourer.Interop.Penumbra; +using Glamourer.Interop.Structs; +using Glamourer.State; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.Material; + +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); + + 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 values = state.Materials.GetValues(min, max); + if (values.Length == 0) + return; + + if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet)) + return; + + for (var i = 0; i < values.Length; ++i) + { + var idx = MaterialValueIndex.FromKey(values[i].key); + var (oldGame, model, source) = values[i].Value; + ref var row = ref baseColorSet[idx.RowIndex]; + if (!idx.DataIndex.TryGetValue(row, out var newGame)) + continue; + + if (newGame == oldGame) + { + idx.DataIndex.SetValue(ref row, model); + } + else + { + switch (source.Base()) + { + case StateSource.Manual: + _stateManager.ChangeMaterialValue(state, idx, Vector3.Zero, Vector3.Zero, ApplySettings.Game); + --i; + break; + case StateSource.Fixed: + idx.DataIndex.SetValue(ref row, model); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, model, source), out _); + break; + } + } + } + + if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture)) + ret = (nint)texture; + } + + [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/MaterialValueIndex.cs b/Glamourer/Interop/Material/MaterialValueIndex.cs index 4cbc116..d2d8db1 100644 --- a/Glamourer/Interop/Material/MaterialValueIndex.cs +++ b/Glamourer/Interop/Material/MaterialValueIndex.cs @@ -59,27 +59,43 @@ public readonly record struct MaterialValueIndex( return true; } - public unsafe bool TryGetTexture(Actor actor, out Texture* texture) + public unsafe bool TryGetTexture(Actor actor, out Texture** texture) { - if (!TryGetTextures(actor, out var textures) || MaterialIndex >= MaterialService.MaterialsPerModel) + if (TryGetTextures(actor, out var textures)) + return TryGetTexture(textures, out texture); + + texture = null; + return false; + } + + public unsafe bool TryGetTexture(ReadOnlySpan> textures, out Texture** texture) + { + if (MaterialIndex >= textures.Length || textures[MaterialIndex].Value == null) { texture = null; return false; } - texture = textures[MaterialIndex].Value; - return texture != null; + fixed (Pointer* ptr = textures) + { + texture = (Texture**)ptr + MaterialIndex; + } + + return true; } public unsafe bool TryGetColorTable(Actor actor, out MtrlFile.ColorTable table) { if (TryGetTexture(actor, out var texture)) - return DirectXTextureHelper.TryGetColorTable(texture, out table); + return TryGetColorTable(texture, out table); table = default; return false; } + public unsafe bool TryGetColorTable(Texture** texture, out MtrlFile.ColorTable table) + => DirectXTextureHelper.TryGetColorTable(*texture, out table); + public unsafe bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row) { if (!TryGetColorTable(actor, out var table)) diff --git a/Glamourer/Interop/Material/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs index 5dd3001..5f7c2dc 100644 --- a/Glamourer/Interop/Material/MaterialValueManager.cs +++ b/Glamourer/Interop/Material/MaterialValueManager.cs @@ -104,7 +104,7 @@ public readonly struct MaterialValueManager public int RemoveValues(MaterialValueIndex min, MaterialValueIndex max) { - var (minIdx, maxIdx) = GetMinMax(CollectionsMarshal.AsSpan(_values), min.Key, max.Key); + var (minIdx, maxIdx) = MaterialValueManager.GetMinMax(CollectionsMarshal.AsSpan(_values), min.Key, max.Key); if (minIdx < 0) return 0; @@ -114,20 +114,49 @@ public readonly struct MaterialValueManager } public ReadOnlySpan<(uint key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) - => Filter(CollectionsMarshal.AsSpan(_values), min, max); + => MaterialValueManager.Filter(CollectionsMarshal.AsSpan(_values), min, max); - public static ReadOnlySpan<(uint Key, T Value)> Filter(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex min, + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private int Search(uint key) + => _values.BinarySearch((key, default!), MaterialValueManager.Comparer.Instance); +} + +public static class MaterialValueManager +{ + internal class Comparer : IComparer<(uint Key, T Value)> + { + public static readonly Comparer Instance = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + int IComparer<(uint Key, T Value)>.Compare((uint Key, T Value) x, (uint Key, T Value) y) + => x.Key.CompareTo(y.Key); + } + + public static bool GetSpecific(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex index, out T ret) + { + var idx = values.BinarySearch((index.Key, default!), Comparer.Instance); + if (idx < 0) + { + ret = default!; + return false; + } + + ret = values[idx].Value; + return true; + } + + public static ReadOnlySpan<(uint Key, T Value)> Filter(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex min, MaterialValueIndex max) { var (minIdx, maxIdx) = GetMinMax(values, min.Key, max.Key); - return minIdx < 0 ? [] : values[minIdx..(maxIdx - minIdx + 1)]; + return minIdx < 0 ? [] : values[minIdx..(maxIdx + 1)]; } /// Obtain the minimum index and maximum index for a minimum and maximum key. - private static (int MinIdx, int MaxIdx) GetMinMax(ReadOnlySpan<(uint Key, T Value)> values, uint minKey, uint maxKey) + internal static (int MinIdx, int MaxIdx) GetMinMax(ReadOnlySpan<(uint Key, T Value)> values, uint minKey, uint maxKey) { // Find the minimum index by binary search. - var idx = values.BinarySearch((minKey, default!), Comparer.Instance); + var idx = values.BinarySearch((minKey, default!), Comparer.Instance); var minIdx = idx; // If the key does not exist, check if it is an invalid range or set it correctly. @@ -152,12 +181,13 @@ public readonly struct MaterialValueManager // Do pretty much the same but in the other direction with the maximum key. - var maxIdx = values[idx..].BinarySearch((maxKey, default!), Comparer.Instance); + var maxIdx = values[idx..].BinarySearch((maxKey, default!), Comparer.Instance); if (maxIdx < 0) { - maxIdx = ~maxIdx; + maxIdx = ~maxIdx + idx; return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1); } + maxIdx += idx; while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey) ++maxIdx; @@ -167,17 +197,4 @@ public readonly struct MaterialValueManager return (minIdx, maxIdx); } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private int Search(uint key) - => _values.BinarySearch((key, default!), Comparer.Instance); - - private class Comparer : IComparer<(uint Key, T Value)> - { - public static readonly Comparer Instance = new(); - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - int IComparer<(uint Key, T Value)>.Compare((uint Key, T Value) x, (uint Key, T Value) y) - => x.Key.CompareTo(y.Key); - } } diff --git a/Glamourer/Interop/Material/PrepareColorSet.cs b/Glamourer/Interop/Material/PrepareColorSet.cs index 4230fc1..1fc2f68 100644 --- a/Glamourer/Interop/Material/PrepareColorSet.cs +++ b/Glamourer/Interop/Material/PrepareColorSet.cs @@ -1,14 +1,11 @@ -using Dalamud.Game; -using Dalamud.Hooking; +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.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; @@ -57,7 +54,8 @@ public sealed unsafe class PrepareColorSet return _task.Result.Original(characterBase, material, stainId); } - public static bool TryGetColorTable(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId, out MtrlFile.ColorTable table) + public static bool TryGetColorTable(CharacterBase* characterBase, MaterialResourceHandle* material, StainId stainId, + out MtrlFile.ColorTable table) { if (material->ColorTable == null) { @@ -66,147 +64,40 @@ public sealed unsafe class PrepareColorSet } var newTable = *(MtrlFile.ColorTable*)material->ColorTable; - if(stainId.Id != 0) + if (stainId.Id != 0) characterBase->ReadStainingTemplate(material, stainId.Id, (Half*)(&newTable)); table = newTable; return true; } -} -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) + /// Assumes the actor is valid. + public static bool TryGetColorTable(Actor actor, MaterialValueIndex index, out MtrlFile.ColorTable table) { - _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); - - 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 values = state.Materials.GetValues(min, max); - if (values.Length == 0) - return; - - if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet)) - return; - - for (var i = 0; i < values.Length; ++i) + var idx = index.SlotIndex * MaterialService.MaterialsPerModel + index.MaterialIndex; + var model = actor.Model.AsCharacterBase; + var handle = (MaterialResourceHandle*)model->Materials[idx]; + if (handle == null) { - var idx = MaterialValueIndex.FromKey(values[i].key); - var (oldGame, model, source) = values[i].Value; - ref var row = ref baseColorSet[idx.RowIndex]; - if (!idx.DataIndex.TryGetValue(row, out var newGame)) - continue; - - if (newGame == oldGame) - { - idx.DataIndex.SetValue(ref row, model); - } - else - { - switch (source.Base()) - { - case StateSource.Manual: - state.Materials.RemoveValue(idx); - --i; - break; - case StateSource.Fixed: - idx.DataIndex.SetValue(ref row, model); - state.Materials.UpdateValue(idx, new MaterialValueState(newGame, model, source), out _); - break; - } - } - } - - if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture)) - ret = (nint)texture; - } - - [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) + table = default; 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 TryGetColorTable(model, handle, GetStain(), out table); - return false; + StainId GetStain() + { + switch (index.DrawObject) + { + case MaterialValueIndex.DrawObjectType.Human: + return index.SlotIndex < 10 ? actor.Model.GetArmor(((uint)index.SlotIndex).ToEquipSlot()).Stain : 0; + case MaterialValueIndex.DrawObjectType.Mainhand: + var mainhand = (Model)actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject; + return mainhand.IsWeapon ? (StainId)mainhand.AsWeapon->ModelUnknown : 0; + case MaterialValueIndex.DrawObjectType.Offhand: + var offhand = (Model)actor.AsCharacter->DrawData.WeaponDataSpan[1].DrawObject; + return offhand.IsWeapon ? (StainId)offhand.AsWeapon->ModelUnknown : 0; + default: return 0; + } + } } } diff --git a/Glamourer/State/InternalStateEditor.cs b/Glamourer/State/InternalStateEditor.cs index 2bc50c3..75cee46 100644 --- a/Glamourer/State/InternalStateEditor.cs +++ b/Glamourer/State/InternalStateEditor.cs @@ -2,6 +2,7 @@ using Glamourer.Designs; using Glamourer.Events; using Glamourer.GameData; +using Glamourer.Interop.Material; using Glamourer.Services; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; @@ -220,6 +221,39 @@ public class InternalStateEditor( return true; } + /// Change the value of a single material color table entry. + public bool ChangeMaterialValue(ActorState state, MaterialValueIndex index, Vector3 value, Vector3 gameValue, StateSource source, out Vector3 oldValue, + uint key = 0) + { + // We already have an existing value. + if (state.Materials.TryGetValue(index, out var old)) + { + oldValue = old.Model; + if (!state.CanUnlock(key)) + return false; + + // Remove if overwritten by a game value. + if (source is StateSource.Game) + { + state.Materials.RemoveValue(index); + return true; + } + + // Update if edited. + state.Materials.UpdateValue(index, new MaterialValueState(gameValue, value, source), out _); + return true; + } + + // We do not have an existing value. + oldValue = gameValue; + // Do not do anything if locked or if the game value updates, because then we do not need to add an entry. + if (!state.CanUnlock(key) || source is StateSource.Game) + return false; + + // Only add an entry if it is sufficiently different from the game value. + return !value.NearEqual(gameValue) && state.Materials.TryAddValue(index, new MaterialValueState(gameValue, value, source)); + } + public bool ChangeMetaState(ActorState state, MetaIndex index, bool value, StateSource source, out bool oldValue, uint key = 0) { diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index e9b74fd..19c1f3e 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -1,6 +1,7 @@ using Glamourer.Designs; using Glamourer.GameData; using Glamourer.Interop; +using Glamourer.Interop.Material; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; @@ -275,6 +276,41 @@ public class StateApplier( return data; } + public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, Vector3? value, bool force) + { + if (!force && !_config.UseAdvancedParameters) + return; + + foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true })) + { + if (!index.TryGetTexture(actor, out var texture)) + continue; + + if (!index.TryGetColorTable(texture, out var table)) + continue; + + Vector3 actualValue; + if (value.HasValue) + actualValue = value.Value; + else if (!PrepareColorSet.TryGetColorTable(actor, index, out var baseTable) + || !index.DataIndex.TryGetValue(baseTable[index.RowIndex], out actualValue)) + continue; + + if (!index.DataIndex.SetValue(ref table[index.RowIndex], actualValue)) + continue; + + MaterialService.ReplaceColorTable(texture, table); + } + } + + public ActorData ChangeMaterialValue(ActorState state, MaterialValueIndex index, bool apply) + { + var data = GetData(state); + if (apply) + ChangeMaterialValue(data, index, state.Materials.TryGetValue(index, out var v) ? v.Model : null, state.IsLocked); + return data; + } + /// Apply the entire state of an actor to all relevant actors, either via immediate redraw or piecewise. /// The state to apply. /// Whether a redraw should be forced. diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index a6606e6..e747448 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -2,6 +2,7 @@ using Glamourer.Designs; using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; +using Glamourer.Interop.Material; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; @@ -149,9 +150,7 @@ public class StateEditor( /// public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue value, ApplySettings settings) { - if (data is not ActorState state) - return; - + var state = (ActorState)data; // Also apply main color to highlights when highlights is off. if (!state.ModelData.Customize.Highlights && flag is CustomizeParameterFlag.HairDiffuse) ChangeCustomizeParameter(state, CustomizeParameterFlag.HairHighlight, value, settings); @@ -166,6 +165,17 @@ public class StateEditor( StateChanged.Invoke(StateChanged.Type.Parameter, settings.Source, state, actors, (old, @new, flag)); } + public void ChangeMaterialValue(object data, MaterialValueIndex index, Vector3 value, Vector3 gameValue, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeMaterialValue(state, index, value, gameValue, settings.Source, out var oldValue, settings.Key)) + return; + + var actors = Applier.ChangeMaterialValue(state, index, settings.Source.RequiresChange()); + Glamourer.Log.Verbose($"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.MaterialValue, settings.Source, state, actors, (oldValue, value, index)); + } + /// public void ChangeMetaState(object data, MetaIndex index, bool value, ApplySettings settings) { From 5e37f8d2e80ce37f94c862144adcd02ab8f4db5a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 2 Feb 2024 23:27:56 +0100 Subject: [PATCH 13/17] add some glamour debug stuff. --- Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs | 2 +- Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs | 3 +- .../Gui/Tabs/DebugTab/GlamourPlatePanel.cs | 135 ++++++++++++++++++ 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs diff --git a/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs b/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs index 35bd136..11f27fd 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs @@ -37,4 +37,4 @@ public class DatFilePanel(ImportService _importService) : IGameDataDrawer ImGui.TextUnformatted(_datFile.Value.Description); } } -} +} \ No newline at end of file diff --git a/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs b/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs index b2b7bdb..2519b84 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs @@ -36,7 +36,8 @@ public class DebugTabHeader(string label, params IGameDataDrawer[] subTrees) provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService() + provider.GetRequiredService(), + provider.GetRequiredService() ); public static DebugTabHeader CreateGameData(IServiceProvider provider) diff --git a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs new file mode 100644 index 0000000..2129c1f --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs @@ -0,0 +1,135 @@ +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game; +using Glamourer.Designs; +using Glamourer.Interop; +using Glamourer.Services; +using Glamourer.State; +using ImGuiNET; +using OtterGui; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public unsafe class GlamourPlatePanel : IGameDataDrawer +{ + private readonly DesignManager _design; + private readonly ItemManager _items; + private readonly StateManager _state; + private readonly ObjectManager _objects; + + public string Label + => "Glamour Plates"; + + public bool Disabled + => false; + + public GlamourPlatePanel(IGameInteropProvider interop, ItemManager items, DesignManager design, StateManager state, ObjectManager objects) + { + _items = items; + _design = design; + _state = state; + _objects = objects; + interop.InitializeFromAttributes(this); + } + + public void Draw() + { + var manager = MirageManager.Instance(); + using (ImRaii.Group()) + { + ImGui.TextUnformatted("Address:"); + ImGui.TextUnformatted("Number of Glamour Plates:"); + ImGui.TextUnformatted("Glamour Plates Requested:"); + ImGui.TextUnformatted("Glamour Plates Loaded:"); + ImGui.TextUnformatted("Is Applying Glamour Plates:"); + } + + ImGui.SameLine(); + using (ImRaii.Group()) + { + ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)manager:X}"); + ImGui.TextUnformatted(manager == null ? "-" : manager->GlamourPlatesSpan.Length.ToString()); + ImGui.TextUnformatted(manager == null ? "-" : manager->GlamourPlatesRequested.ToString()); + ImGui.SameLine(); + if (ImGui.SmallButton("Request Update")) + RequestGlamour(); + ImGui.TextUnformatted(manager == null ? "-" : manager->GlamourPlatesLoaded.ToString()); + ImGui.TextUnformatted(manager == null ? "-" : manager->IsApplyingGlamourPlate.ToString()); + } + + if (manager == null) + return; + + ActorState? state = null; + var (identifier, data) = _objects.PlayerData; + var enabled = data.Valid && _state.GetOrCreate(identifier, data.Objects[0], out state); + + for (var i = 0; i < manager->GlamourPlatesSpan.Length; ++i) + { + using var tree = ImRaii.TreeNode($"Plate #{i + 1:D2}"); + if (!tree) + continue; + + ref var plate = ref manager->GlamourPlatesSpan[i]; + if (ImGuiUtil.DrawDisabledButton("Apply to Player", Vector2.Zero, string.Empty, !enabled)) + { + var design = CreateDesign(plate); + _state.ApplyDesign(state!, design, ApplySettings.Manual); + } + + using (ImRaii.Group()) + { + foreach (var slot in EquipSlotExtensions.FullSlots) + ImGui.TextUnformatted(slot.ToName()); + } + + ImGui.SameLine(); + using (ImRaii.Group()) + { + foreach (var (_, index) in EquipSlotExtensions.FullSlots.WithIndex()) + ImGui.TextUnformatted($"{plate.ItemIds[index]:D6}, {plate.StainIds[index]:D3}"); + } + } + } + + [Signature("E8 ?? ?? ?? ?? 32 C0 48 8B 5C 24 ?? 48 8B 6C 24 ?? 48 83 C4 ?? 5F")] + private readonly delegate* unmanaged _requestUpdate = null!; + + public void RequestGlamour() + { + var manager = MirageManager.Instance(); + if (manager == null) + return; + + _requestUpdate(manager); + } + + public DesignBase CreateDesign(in MirageManager.GlamourPlate plate) + { + var design = _design.CreateTemporary(); + design.ApplyCustomize = 0; + design.ApplyCrest = 0; + design.ApplyMeta = 0; + design.ApplyParameters = 0; + design.ApplyEquip = 0; + foreach (var (slot, index) in EquipSlotExtensions.FullSlots.WithIndex()) + { + var itemId = plate.ItemIds[index]; + if (itemId == 0) + continue; + + var item = _items.Resolve(slot, itemId); + if (!item.Valid) + continue; + + design.GetDesignDataRef().SetItem(slot, item); + design.GetDesignDataRef().SetStain(slot, plate.StainIds[index]); + design.ApplyEquip |= slot.ToBothFlags(); + } + + return design; + } +} From 1fefe7366c07afc43ec91328123b58f45aa4e749 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Feb 2024 15:21:29 +0100 Subject: [PATCH 14/17] MouseWheels are pretty great. --- Glamourer/Designs/Design.cs | 4 + Glamourer/Designs/DesignManager.cs | 12 +++ Glamourer/Events/DesignChanged.cs | 3 + .../CustomizationDrawer.Color.cs | 5 + .../Customization/CustomizationDrawer.Icon.cs | 12 ++- .../CustomizationDrawer.Simple.cs | 102 ++++++++++++------ Glamourer/Gui/DesignCombo.cs | 100 ++++++++++------- Glamourer/Gui/DesignQuickBar.cs | 7 +- .../Gui/Equipment/GlamourerColorCombo.cs | 5 +- Glamourer/Gui/Equipment/ItemCombo.cs | 2 +- Glamourer/Gui/Equipment/WeaponCombo.cs | 2 +- .../Gui/Tabs/AutomationTab/HumanNpcCombo.cs | 2 +- Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs | 2 +- .../Gui/Tabs/DesignTab/DesignColorCombo.cs | 9 +- .../Gui/Tabs/DesignTab/DesignDetailTab.cs | 15 ++- .../Gui/Tabs/DesignTab/DesignLinkDrawer.cs | 2 +- Glamourer/Gui/Tabs/DesignTab/ModCombo.cs | 2 +- .../Gui/Tabs/DesignTab/MultiDesignPanel.cs | 43 +++++++- Glamourer/Gui/Tabs/NpcCombo.cs | 2 +- Glamourer/Services/ServiceManager.cs | 3 +- OtterGui | 2 +- Penumbra.GameData | 2 +- 22 files changed, 250 insertions(+), 88 deletions(-) diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 89dd62f..20d85e7 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -26,6 +26,7 @@ public sealed class Design : DesignBase, ISavable { Tags = [.. other.Tags]; Description = other.Description; + QuickDesign = other.QuickDesign; AssociatedMods = new SortedList(other.AssociatedMods); } @@ -39,6 +40,7 @@ public sealed class Design : DesignBase, ISavable public string Description { get; internal set; } = string.Empty; public string[] Tags { get; internal set; } = []; public int Index { get; internal set; } + public bool QuickDesign { get; internal set; } = true; public string Color { get; internal set; } = string.Empty; public SortedList AssociatedMods { get; private set; } = []; public LinkContainer Links { get; private set; } = []; @@ -64,6 +66,7 @@ public sealed class Design : DesignBase, ISavable ["Name"] = Name.Text, ["Description"] = Description, ["Color"] = Color, + ["QuickDesign"] = QuickDesign, ["Tags"] = JArray.FromObject(Tags), ["WriteProtected"] = WriteProtected(), ["Equipment"] = SerializeEquipment(), @@ -124,6 +127,7 @@ public sealed class Design : DesignBase, ISavable Description = json["Description"]?.ToObject() ?? string.Empty, Tags = ParseTags(json), LastEdit = json["LastEdit"]?.ToObject() ?? creationDate, + QuickDesign = json["QuickDesign"]?.ToObject() ?? true, }; if (design.LastEdit < creationDate) design.LastEdit = creationDate; diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index c3d8664..da6ed90 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -284,6 +284,18 @@ public sealed class DesignManager : DesignEditor DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, value); } + /// Set the quick design bar display status of a design. + public void SetQuickDesign(Design design, bool value) + { + if (value == design.QuickDesign) + return; + + design.QuickDesign = value; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar."); + DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, value); + } + #endregion #region Edit Application Rules diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index f3ebb5f..81d56e4 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -92,6 +92,9 @@ public sealed class DesignChanged() /// An existing design changed its write protection status. Data is the new value [bool]. WriteProtection, + /// An existing design changed its display status for the quick design bar. Data is the new value [bool]. + QuickDesignBar, + /// An existing design changed one of the meta flags. Data is the flag, whether it was about their applying and the new value [(MetaFlag, bool, bool)]. Other, } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs index f3753ae..ff6e0c5 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs @@ -23,6 +23,11 @@ public partial class CustomizationDrawer { if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.None, _framedIconSize)) ImGui.OpenPopup(ColorPickerPopupName); + else if (current >= 0 && CaptureMouseWheel(ref current, 0, _currentCount)) + { + var data = _set.Data(_currentIndex, current, _customize.Face); + UpdateValue(data.Value); + } } var npc = false; diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs index 4e0f3de..c0c45d2 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs @@ -18,8 +18,9 @@ public partial class CustomizationDrawer using var bigGroup = ImRaii.Group(); var label = _currentOption; - var current = _set.DataByValue(index, _currentByte, out var custom, _customize.Face); - var npc = false; + var current = _set.DataByValue(index, _currentByte, out var custom, _customize.Face); + var originalCurrent = current; + var npc = false; if (current < 0) { label = $"{_currentOption} (NPC)"; @@ -32,7 +33,14 @@ public partial class CustomizationDrawer using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw)) { if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + { ImGui.OpenPopup(IconSelectorPopup); + } + else if (originalCurrent >= 0 && CaptureMouseWheel(ref current, 0, _currentCount)) + { + var data = _set.Data(_currentIndex, current, _customize.Face); + UpdateValue(data.Value); + } } ImGuiUtil.HoverIconTooltip(icon, _iconSize); diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs index 02d4def..3a8e021 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -1,6 +1,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGuiInternal; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -34,7 +35,8 @@ public partial class CustomizationDrawer { var tmp = (int)_currentByte.Value; ImGui.SetNextItemWidth(_comboSelectorSize); - if (ImGui.SliderInt("##slider", ref tmp, 0, _currentCount - 1, "%i", ImGuiSliderFlags.AlwaysClamp)) + if (ImGui.SliderInt("##slider", ref tmp, 0, _currentCount - 1, "%i", ImGuiSliderFlags.AlwaysClamp) + || CaptureMouseWheel(ref tmp, 0, _currentCount - 1)) UpdateValue((CustomizeValue)tmp); } @@ -42,11 +44,10 @@ public partial class CustomizationDrawer { var tmp = (int)_currentByte.Value; ImGui.SetNextItemWidth(_inputIntSize); + var cap = ImGui.GetIO().KeyCtrl ? byte.MaxValue : _currentCount - 1; if (ImGui.InputInt("##text", ref tmp, 1, 1)) { - var newValue = (CustomizeValue)(ImGui.GetIO().KeyCtrl - ? Math.Clamp(tmp, 0, byte.MaxValue) - : Math.Clamp(tmp, 0, _currentCount - 1)); + var newValue = (CustomizeValue)Math.Clamp(tmp, 0, cap); UpdateValue(newValue); } @@ -73,6 +74,10 @@ public partial class CustomizationDrawer else if (ImGui.GetIO().KeyCtrl) UpdateValue((CustomizeValue)value); } + else + { + CheckWheel(); + } if (!_withApply) ImGuiUtil.HoverTooltip("Hold Control to force updates with invalid/unknown options at your own risk."); @@ -81,15 +86,29 @@ public partial class CustomizationDrawer if (ImGuiUtil.DrawDisabledButton("-", new Vector2(ImGui.GetFrameHeight()), "Select the previous available option in order.", currentIndex <= 0)) UpdateValue(_set.Data(_currentIndex, currentIndex - 1, _customize.Face).Value); + else + CheckWheel(); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("+", new Vector2(ImGui.GetFrameHeight()), "Select the next available option in order.", currentIndex >= _currentCount - 1 || npc)) UpdateValue(_set.Data(_currentIndex, currentIndex + 1, _customize.Face).Value); + else + CheckWheel(); + return; + + void CheckWheel() + { + if (currentIndex < 0 || !CaptureMouseWheel(ref currentIndex, 0, _currentCount)) + return; + + var data = _set.Data(_currentIndex, currentIndex, _customize.Face); + UpdateValue(data.Value); + } } private void DrawListSelector(CustomizeIndex index, bool indexedBy1) { - using var id = SetId(index); + using var id = SetId(index); using var bigGroup = ImRaii.Group(); using (_ = ImRaii.Disabled(_locked)) @@ -122,29 +141,31 @@ public partial class CustomizationDrawer private void ListCombo0() { ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); - var current = _currentByte.Value; - using var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current + 1}"); - - if (!combo) - return; - - for (var i = 0; i < _currentCount; ++i) + var current = (int)_currentByte.Value; + using (var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current + 1}")) { - if (ImGui.Selectable($"{_currentOption} #{i + 1}##combo", i == current)) - UpdateValue((CustomizeValue)i); + if (combo) + + for (var i = 0; i < _currentCount; ++i) + { + if (ImGui.Selectable($"{_currentOption} #{i + 1}##combo", i == current)) + UpdateValue((CustomizeValue)i); + } } + + if (CaptureMouseWheel(ref current, 0, _currentCount)) + UpdateValue((CustomizeValue)current); } private void ListInputInt0() { var tmp = _currentByte.Value + 1; ImGui.SetNextItemWidth(_inputIntSize); + var cap = ImGui.GetIO().KeyCtrl ? byte.MaxValue + 1 : _currentCount; if (ImGui.InputInt("##text", ref tmp, 1, 1)) { - var newValue = (CustomizeValue)(ImGui.GetIO().KeyCtrl - ? Math.Clamp(tmp, 1, byte.MaxValue + 1) - : Math.Clamp(tmp, 1, _currentCount)); - UpdateValue(newValue - 1); + var newValue = Math.Clamp(tmp, 1, cap); + UpdateValue((CustomizeValue)(newValue - 1)); } ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]\n" @@ -154,28 +175,29 @@ public partial class CustomizationDrawer private void ListCombo1() { ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); - var current = _currentByte.Value; - using var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current}"); - - if (!combo) - return; - - for (var i = 1; i <= _currentCount; ++i) + var current = (int)_currentByte.Value; + using (var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current}")) { - if (ImGui.Selectable($"{_currentOption} #{i}##combo", i == current)) - UpdateValue((CustomizeValue)i); + if (combo) + for (var i = 1; i <= _currentCount; ++i) + { + if (ImGui.Selectable($"{_currentOption} #{i}##combo", i == current)) + UpdateValue((CustomizeValue)i); + } } + + if (CaptureMouseWheel(ref current, 1, _currentCount)) + UpdateValue((CustomizeValue)current); } private void ListInputInt1() { var tmp = (int)_currentByte.Value; ImGui.SetNextItemWidth(_inputIntSize); + var (offset, cap) = ImGui.GetIO().KeyCtrl ? (0, byte.MaxValue) : (1, _currentCount); if (ImGui.InputInt("##text", ref tmp, 1, 1)) { - var newValue = (CustomizeValue)(ImGui.GetIO().KeyCtrl - ? Math.Clamp(tmp, 0, byte.MaxValue) - : Math.Clamp(tmp, 1, _currentCount)); + var newValue = (CustomizeValue)Math.Clamp(tmp, offset, cap); UpdateValue(newValue); } @@ -183,6 +205,26 @@ public partial class CustomizationDrawer + "Hold Control to force updates with invalid/unknown options at your own risk."); } + private static bool CaptureMouseWheel(ref int value, int offset, int cap) + { + if (!ImGui.IsItemHovered()) + return false; + + ImGuiInternal.ItemSetUsingMouseWheel(); + + var mw = (int)ImGui.GetIO().MouseWheel; + if (mw == 0) + return false; + + value -= offset; + value = mw switch + { + < 0 => offset + (value + cap + mw) % cap, + _ => offset + (value + mw) % cap, + }; + return true; + } + // Draw a customize checkbox. private void DrawCheckbox(CustomizeIndex idx) { diff --git a/Glamourer/Gui/DesignCombo.cs b/Glamourer/Gui/DesignCombo.cs index e255c17..f5e3272 100644 --- a/Glamourer/Gui/DesignCombo.cs +++ b/Glamourer/Gui/DesignCombo.cs @@ -1,7 +1,6 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Glamourer.Automation; -using Glamourer.GameData; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Services; @@ -25,7 +24,7 @@ public abstract class DesignComboBase : FilterComboCache>, protected DesignComboBase(Func>> generator, Logger log, DesignChanged designChanged, TabSelected tabSelected, EphemeralConfig config, DesignColors designColors) - : base(generator, log) + : base(generator, MouseWheelType.Unmodified, log) { _designChanged = designChanged; TabSelected = tabSelected; @@ -38,7 +37,10 @@ public abstract class DesignComboBase : FilterComboCache>, => _config.IncognitoMode; void IDisposable.Dispose() - => _designChanged.Unsubscribe(OnDesignChange); + { + _designChanged.Unsubscribe(OnDesignChange); + GC.SuppressFinalize(this); + } protected override bool DrawSelectable(int globalIdx, bool selected) { @@ -118,63 +120,87 @@ public abstract class DesignComboBase : FilterComboCache>, { case DesignChanged.Type.Created: case DesignChanged.Type.Renamed: - Cleanup(); - break; + case DesignChanged.Type.ChangedColor: case DesignChanged.Type.Deleted: - Cleanup(); - if (CurrentSelection?.Item1 == design) + case DesignChanged.Type.QuickDesignBar: + var priorState = IsInitialized; + if (priorState) + Cleanup(); + CurrentSelectionIdx = Items.IndexOf(s => ReferenceEquals(s.Item1, CurrentSelection?.Item1)); + if (CurrentSelectionIdx >= 0) { - CurrentSelectionIdx = Items.Count > 0 ? 0 : -1; - CurrentSelection = Items[CurrentSelectionIdx]; + CurrentSelection = Items[CurrentSelectionIdx]; + } + else if (Items.Count > 0) + { + CurrentSelectionIdx = 0; + CurrentSelection = Items[0]; + } + else + { + CurrentSelection = null; } + if (!priorState) + Cleanup(); break; } } } -public sealed class DesignCombo : DesignComboBase +public abstract class DesignCombo : DesignComboBase { - private readonly DesignManager _manager; - - public DesignCombo(DesignManager designs, DesignFileSystem fileSystem, Logger log, DesignChanged designChanged, TabSelected tabSelected, - EphemeralConfig config, DesignColors designColors) - : base(() => designs.Designs - .Select(d => new Tuple(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) - .OrderBy(d => d.Item2) - .ToList(), log, designChanged, tabSelected, config, designColors) + protected DesignCombo(Logger log, DesignChanged designChanged, TabSelected tabSelected, + EphemeralConfig config, DesignColors designColors, Func>> generator) + : base(generator, log, designChanged, tabSelected, config, designColors) { - _manager = designs; - if (designs.Designs.Count == 0) + if (Items.Count == 0) return; CurrentSelection = Items[0]; CurrentSelectionIdx = 0; + base.Cleanup(); } public Design? Design => CurrentSelection?.Item1; public void Draw(float width) - { - Draw(Design, (Incognito ? Design?.Incognito : Design?.Name.Text) ?? string.Empty, width); - if (ImGui.IsItemHovered() && _manager.Designs.Count > 1) - { - var mouseWheel = -(int)ImGui.GetIO().MouseWheel % _manager.Designs.Count; - CurrentSelectionIdx = mouseWheel switch - { - < 0 when CurrentSelectionIdx < 0 => _manager.Designs.Count - 1 + mouseWheel, - < 0 => (CurrentSelectionIdx + _manager.Designs.Count + mouseWheel) % _manager.Designs.Count, - > 0 when CurrentSelectionIdx < 0 => mouseWheel, - > 0 => (CurrentSelectionIdx + mouseWheel) % _manager.Designs.Count, - _ => CurrentSelectionIdx, - }; - CurrentSelection = Items[CurrentSelectionIdx]; - } - } + => Draw(Design, (Incognito ? Design?.Incognito : Design?.Name.Text) ?? string.Empty, width); } -public sealed class RevertDesignCombo : DesignComboBase, IDisposable +public sealed class QuickDesignCombo( + DesignManager designs, + DesignFileSystem fileSystem, + Logger log, + DesignChanged designChanged, + TabSelected tabSelected, + EphemeralConfig config, + DesignColors designColors) + : DesignCombo(log, designChanged, tabSelected, config, designColors, () => + [ + .. designs.Designs + .Where(d => d.QuickDesign) + .Select(d => new Tuple(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) + .OrderBy(d => d.Item2), + ]); + +public sealed class LinkDesignCombo( + DesignManager designs, + DesignFileSystem fileSystem, + Logger log, + DesignChanged designChanged, + TabSelected tabSelected, + EphemeralConfig config, + DesignColors designColors) + : DesignCombo(log, designChanged, tabSelected, config, designColors, () => + [ + .. designs.Designs + .Select(d => new Tuple(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)) + .OrderBy(d => d.Item2), + ]); + +public sealed class RevertDesignCombo : DesignComboBase { public const int RevertDesignIndex = -1228; public readonly Design RevertDesign; diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs index 78e813e..1ad28b0 100644 --- a/Glamourer/Gui/DesignQuickBar.cs +++ b/Glamourer/Gui/DesignQuickBar.cs @@ -24,7 +24,7 @@ public sealed class DesignQuickBar : Window, IDisposable : ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing; private readonly Configuration _config; - private readonly DesignCombo _designCombo; + private readonly QuickDesignCombo _designCombo; private readonly StateManager _stateManager; private readonly AutoDesignApplier _autoDesignApplier; private readonly ObjectManager _objects; @@ -34,7 +34,7 @@ public sealed class DesignQuickBar : Window, IDisposable private DateTime _keyboardToggle = DateTime.UnixEpoch; private int _numButtons; - public DesignQuickBar(Configuration config, DesignCombo designCombo, StateManager stateManager, IKeyState keyState, + public DesignQuickBar(Configuration config, QuickDesignCombo designCombo, StateManager stateManager, IKeyState keyState, ObjectManager objects, AutoDesignApplier autoDesignApplier) : base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking) { @@ -299,7 +299,8 @@ public sealed class DesignQuickBar : Window, IDisposable (true, false) => 3, (false, false) => 2, }; - Size = new Vector2((7 + _numButtons) * ImGui.GetFrameHeight() + _numButtons * ImGui.GetStyle().ItemInnerSpacing.X, ImGui.GetFrameHeight()); + Size = new Vector2((7 + _numButtons) * ImGui.GetFrameHeight() + _numButtons * ImGui.GetStyle().ItemInnerSpacing.X, + ImGui.GetFrameHeight()); return Size.Value.X; } } diff --git a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs index 39d63dc..3a791d0 100644 --- a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs +++ b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs @@ -10,7 +10,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Gui.Equipment; public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, FavoriteManager _favorites) - : FilterComboColors(_comboWidth, CreateFunc(_stains, _favorites), Glamourer.Log) + : FilterComboColors(_comboWidth, MouseWheelType.Unmodified, CreateFunc(_stains, _favorites), Glamourer.Log) { protected override bool DrawSelectable(int globalIdx, bool selected) { @@ -36,6 +36,9 @@ public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, Fa return base.DrawSelectable(globalIdx, selected); } + public override bool Draw(string label, uint color, string name, bool found, bool gloss, float previewWidth) + => base.Draw(label, color, name, found, gloss, previewWidth); + private static Func>> CreateFunc(DictStain stains, FavoriteManager favorites) => () => stains.Select(kvp => (kvp, favorites.Contains(kvp.Key))).OrderBy(p => !p.Item2).Select(p => p.kvp) diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/Glamourer/Gui/Equipment/ItemCombo.cs index d9fd12a..a6f22b5 100644 --- a/Glamourer/Gui/Equipment/ItemCombo.cs +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -24,7 +24,7 @@ public sealed class ItemCombo : FilterComboCache public Variant CustomVariant { get; private set; } public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites) - : base(() => GetItems(favorites, items, slot), log) + : base(() => GetItems(favorites, items, slot), MouseWheelType.Unmodified, log) { _favorites = favorites; Label = GetLabel(gameData, slot); diff --git a/Glamourer/Gui/Equipment/WeaponCombo.cs b/Glamourer/Gui/Equipment/WeaponCombo.cs index 32f383b..b6c3218 100644 --- a/Glamourer/Gui/Equipment/WeaponCombo.cs +++ b/Glamourer/Gui/Equipment/WeaponCombo.cs @@ -17,7 +17,7 @@ public sealed class WeaponCombo : FilterComboCache private float _innerWidth; public WeaponCombo(ItemManager items, FullEquipType type, Logger log) - : base(() => GetWeapons(items, type), log) + : base(() => GetWeapons(items, type), MouseWheelType.Unmodified, log) { Label = GetLabel(type); SearchByParts = true; diff --git a/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs index 49feae9..530e04a 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs @@ -16,7 +16,7 @@ public sealed class HumanNpcCombo( DictBNpc bNpcs, HumanModelList humans, Logger log) - : FilterComboCache<(string Name, ObjectKind Kind, uint[] Ids)>(() => CreateList(modelCharaDict, bNpcNames, bNpcs, humans), log) + : FilterComboCache<(string Name, ObjectKind Kind, uint[] Ids)>(() => CreateList(modelCharaDict, bNpcNames, bNpcs, humans), MouseWheelType.None, log) { protected override string ToString((string Name, ObjectKind Kind, uint[] Ids) obj) => obj.Name; diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index 0387d58..2f4e1d8 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -429,7 +429,7 @@ public class SetPanel( } private sealed class JobGroupCombo(AutoDesignManager manager, JobService jobs, Logger log) - : FilterComboCache(() => jobs.JobGroups.Values.ToList(), log) + : FilterComboCache(() => jobs.JobGroups.Values.ToList(), MouseWheelType.None, log) { public void Draw(AutoDesignSet set, AutoDesign design, int autoDesignIndex) { diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs index d00fea8..72d717c 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs @@ -10,8 +10,15 @@ public sealed class DesignColorCombo(DesignColors _designColors, bool _skipAutom FilterComboCache(_skipAutomatic ? _designColors.Keys.OrderBy(k => k) : _designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName), - Glamourer.Log) + MouseWheelType.Shift, Glamourer.Log) { + protected override void OnMouseWheel(string preview, ref int current, int steps) + { + if (CurrentSelectionIdx < 0) + CurrentSelectionIdx = Items.IndexOf(preview); + base.OnMouseWheel(preview, ref current, steps); + } + protected override bool DrawSelectable(int globalIdx, bool selected) { var isAutomatic = !_skipAutomatic && globalIdx == 0; diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs index ecbf0e7..e56ec00 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs @@ -125,10 +125,23 @@ public class DesignDetailTab Glamourer.Messager.NotificationMessage(ex, ex.Message, "Could not rename or move design", NotificationType.Error); } + ImGuiUtil.DrawFrameColumn("Quick Design Bar"); + ImGui.TableNextColumn(); + if (ImGui.RadioButton("Display##qdb", _selector.Selected.QuickDesign)) + _manager.SetQuickDesign(_selector.Selected!, true); + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + if (ImGui.RadioButton("Hide##qdb", !_selector.Selected.QuickDesign)) + _manager.SetQuickDesign(_selector.Selected!, false); + if (hovered || ImGui.IsItemHovered()) + ImGui.SetTooltip("Display or hide this design in your quick design bar."); + ImGuiUtil.DrawFrameColumn("Color"); var colorName = _selector.Selected!.Color.Length == 0 ? DesignColors.AutomaticName : _selector.Selected!.Color; ImGui.TableNextColumn(); - if (_colorCombo.Draw("##colorCombo", colorName, "Associate a color with this design. Right-Click to revert to automatic coloring.", + if (_colorCombo.Draw("##colorCombo", colorName, "Associate a color with this design.\n" + + "Right-Click to revert to automatic coloring.\n" + + "Hold Control and scroll the mousewheel to scroll.", width.X - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight()) && _colorCombo.CurrentSelection != null) { diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs b/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs index f45f936..7b996e8 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs @@ -10,7 +10,7 @@ using OtterGui.Services; namespace Glamourer.Gui.Tabs.DesignTab; -public class DesignLinkDrawer(DesignLinkManager _linkManager, DesignFileSystemSelector _selector, DesignCombo _combo) : IUiService +public class DesignLinkDrawer(DesignLinkManager _linkManager, DesignFileSystemSelector _selector, LinkDesignCombo _combo) : IUiService { private int _dragDropIndex = -1; private LinkOrder _dragDropOrder = LinkOrder.None; diff --git a/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs b/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs index 4cd899f..53501b0 100644 --- a/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs +++ b/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs @@ -11,7 +11,7 @@ namespace Glamourer.Gui.Tabs.DesignTab; public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> { public ModCombo(PenumbraService penumbra, Logger log) - : base(penumbra.GetMods, log) + : base(penumbra.GetMods, MouseWheelType.None, log) { SearchByParts = false; } diff --git a/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs index 474c2f4..6e5b1b1 100644 --- a/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using Glamourer.Designs; using ImGuiNET; using OtterGui; +using OtterGui.Filesystem; using OtterGui.Raii; namespace Glamourer.Gui.Tabs.DesignTab; @@ -21,6 +22,7 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager DrawDesignList(); var offset = DrawMultiTagger(width); DrawMultiColor(width, offset); + DrawMultiQuickDesignBar(offset); } private void DrawDesignList() @@ -35,6 +37,8 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager var sizeMods = availableSizePercent * 35; var sizeFolders = availableSizePercent * 65; + _numQuickDesignEnabled = 0; + _numDesigns = 0; using (var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg)) { if (!table) @@ -61,15 +65,24 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(fullName); + + if (path is not DesignFileSystem.Leaf l2) + continue; + + ++_numDesigns; + if (l2.Value.QuickDesign) + ++_numQuickDesignEnabled; } } ImGui.Separator(); } - private string _tag = string.Empty; - private readonly List _addDesigns = []; - private readonly List<(Design, int)> _removeDesigns = []; + private string _tag = string.Empty; + private int _numQuickDesignEnabled; + private int _numDesigns; + private readonly List _addDesigns = []; + private readonly List<(Design, int)> _removeDesigns = []; private float DrawMultiTagger(Vector2 width) { @@ -110,6 +123,30 @@ public class MultiDesignPanel(DesignFileSystemSelector _selector, DesignManager return offset; } + private void DrawMultiQuickDesignBar(float offset) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Multi QDB:"); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numQuickDesignEnabled; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs are already displayed in the quick design bar." + : $"Display all {_numDesigns} selected designs in the quick design bar. Changes {diff} designs."; + if (ImGuiUtil.DrawDisabledButton("Display Selected Designs in QDB", buttonWidth, tt, diff == 0)) + foreach(var design in _selector.SelectedPaths.OfType()) + _editor.SetQuickDesign(design.Value, true); + + ImGui.SameLine(); + tt = _numQuickDesignEnabled == 0 + ? $"All {_numDesigns} selected designs are already hidden in the quick design bar." + : $"Hide all {_numDesigns} selected designs in the quick design bar. Changes {_numQuickDesignEnabled} designs."; + if (ImGuiUtil.DrawDisabledButton("Hide Selected Designs in QDB", buttonWidth, tt, _numQuickDesignEnabled == 0)) + foreach (var design in _selector.SelectedPaths.OfType()) + _editor.SetQuickDesign(design.Value, false); + ImGui.Separator(); + } + private void DrawMultiColor(Vector2 width, float offset) { ImGui.AlignTextToFramePadding(); diff --git a/Glamourer/Gui/Tabs/NpcCombo.cs b/Glamourer/Gui/Tabs/NpcCombo.cs index 4b1274c..86eb766 100644 --- a/Glamourer/Gui/Tabs/NpcCombo.cs +++ b/Glamourer/Gui/Tabs/NpcCombo.cs @@ -4,7 +4,7 @@ using OtterGui.Widgets; namespace Glamourer.Gui.Tabs; public class NpcCombo(NpcCustomizeSet npcCustomizeSet) - : FilterComboCache(npcCustomizeSet, Glamourer.Log) + : FilterComboCache(npcCustomizeSet, MouseWheelType.None, Glamourer.Log) { protected override string ToString(NpcData obj) => obj.Name; diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index dd06e3a..a5cc0ee 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -146,7 +146,8 @@ public static class ServiceManagerA .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/OtterGui b/OtterGui index 04eb0b5..2d8a03e 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 04eb0b5ed3930e9cb87ad00dffa9c8be90b58bb3 +Subproject commit 2d8a03eebd80e19c6936a28ab2e3a8c164cc17f3 diff --git a/Penumbra.GameData b/Penumbra.GameData index 260ac69..fb18c80 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 260ac69cd6f17050eaf9b7e0b5ce9a8843edfee4 +Subproject commit fb18c80551203a1cf6cd01ec2b0850fbc8e44240 From 42ac507b863c2884816cc9706dc6a68e77ca172c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Feb 2024 16:21:21 +0100 Subject: [PATCH 15/17] Improve festivals and add dolphins. --- Glamourer/Services/CodeService.cs | 11 ++++++++--- Glamourer/State/FunEquipSet.cs | 11 +++++++++-- Glamourer/State/FunModule.cs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/Glamourer/Services/CodeService.cs b/Glamourer/Services/CodeService.cs index 77fdcaa..71f1a45 100644 --- a/Glamourer/Services/CodeService.cs +++ b/Glamourer/Services/CodeService.cs @@ -29,10 +29,11 @@ public class CodeService World = 0x010000, Elephants = 0x020000, Crown = 0x040000, + Dolphins = 0x080000, } - public const CodeFlag DyeCodes = CodeFlag.Clown | CodeFlag.World | CodeFlag.Elephants; - public const CodeFlag GearCodes = CodeFlag.Emperor | CodeFlag.World | CodeFlag.Elephants; + public const CodeFlag DyeCodes = CodeFlag.Clown | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins; + public const CodeFlag GearCodes = CodeFlag.Emperor | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins; public const CodeFlag RaceCodes = CodeFlag.OopsHyur | CodeFlag.OopsElezen @@ -114,7 +115,9 @@ public class CodeService return null; var badFlags = ~GetMutuallyExclusive(flag); - return v => _enabled = v ? (_enabled | flag) & badFlags : _enabled & ~flag;; + return v => _enabled = v ? (_enabled | flag) & badFlags : _enabled & ~flag; + + ; } public CodeFlag GetCode(string name) @@ -173,6 +176,7 @@ public class CodeService CodeFlag.World => (DyeCodes | GearCodes) & ~CodeFlag.World, CodeFlag.Elephants => (DyeCodes | GearCodes) & ~CodeFlag.Elephants, CodeFlag.Crown => 0, + CodeFlag.Dolphins => (DyeCodes | GearCodes) & ~CodeFlag.Dolphins, _ => 0, }; @@ -198,6 +202,7 @@ public class CodeService CodeFlag.World => [ 0xFD, 0xA2, 0xD2, 0xBC, 0xD9, 0x8A, 0x7E, 0x2B, 0x52, 0xCB, 0x57, 0x6E, 0x3A, 0x2E, 0x30, 0xBA, 0x4E, 0xAE, 0x42, 0xEA, 0x5C, 0x57, 0xDF, 0x17, 0x37, 0x3C, 0xCE, 0x17, 0x42, 0x43, 0xAE, 0xD0 ], CodeFlag.Elephants => [ 0x9F, 0x4C, 0xCF, 0x6D, 0xC4, 0x01, 0x31, 0x46, 0x02, 0x05, 0x31, 0xED, 0xED, 0xB2, 0x66, 0x29, 0x31, 0x09, 0x1E, 0xE7, 0x47, 0xDE, 0x7B, 0x03, 0xB0, 0x3C, 0x06, 0x76, 0x26, 0x91, 0xDF, 0xB2 ], CodeFlag.Crown => [ 0x43, 0x8E, 0x34, 0x56, 0x24, 0xC9, 0xC6, 0xDE, 0x2A, 0x68, 0x3A, 0x5D, 0xF5, 0x8E, 0xCB, 0xEF, 0x0D, 0x4D, 0x5B, 0xDC, 0x23, 0xF9, 0xF9, 0xBD, 0xD9, 0x60, 0xAD, 0x53, 0xC5, 0xA0, 0x33, 0xC4 ], + CodeFlag.Dolphins => [ 0x64, 0xC6, 0x2E, 0x7C, 0x22, 0x3A, 0x42, 0xF5, 0xC3, 0x93, 0x4F, 0x70, 0x1F, 0xFD, 0xFA, 0x3C, 0x98, 0xD2, 0x7C, 0xD8, 0x88, 0xA7, 0x3D, 0x1D, 0x0D, 0xD6, 0x70, 0x15, 0x28, 0x2E, 0x79, 0xE7 ], _ => [], }; } diff --git a/Glamourer/State/FunEquipSet.cs b/Glamourer/State/FunEquipSet.cs index bb56fcb..91e6419 100644 --- a/Glamourer/State/FunEquipSet.cs +++ b/Glamourer/State/FunEquipSet.cs @@ -77,7 +77,8 @@ internal class FunEquipSet new Group(0000, 0, 0137, 2, 0000, 0, 0000, 0, 0000, 0), // Wailing Spirit new Group(0232, 1, 0232, 1, 0279, 1, 0232, 1, 0232, 1), // Eerie Attire new Group(0232, 1, 6036, 1, 0279, 1, 0232, 1, 0232, 1), // Vampire - new Group(0505, 6, 0505, 6, 0505, 6, 0505, 6, 0505, 6) // Manusya Casting + new Group(0505, 6, 0505, 6, 0505, 6, 0505, 6, 0505, 6), // Manusya Casting + new Group(6147, 1, 6147, 1, 6147, 1, 6147, 1, 6147, 1) // Tonberry ); public static readonly FunEquipSet AprilFirst = new @@ -94,7 +95,13 @@ internal class FunEquipSet new Group(0159, 1, 0000, 0, 0000, 0, 0000, 0, 0000, 0), // Slime Crown new Group(6117, 1, 6117, 1, 6117, 1, 6117, 1, 6117, 1), // Clown new Group(6169, 3, 6169, 3, 0279, 1, 6169, 3, 6169, 3), // Chocobo Pajama - new Group(6169, 2, 6169, 2, 0279, 2, 6169, 2, 6169, 2) // Cactuar Pajama + new Group(6169, 2, 6169, 2, 0279, 2, 6169, 2, 6169, 2), // Cactuar Pajama + new Group(6023, 1, 6023, 1, 0000, 0, 0000, 0, 0000, 0), // Swine + new Group(5040, 1, 0000, 0, 0000, 0, 0000, 0, 0000, 0), // Namazu only + new Group(5040, 1, 6023, 1, 0000, 0, 0000, 0, 0000, 0), // Namazu lean + new Group(5040, 1, 6023, 1, 0000, 0, 0000, 0, 0000, 0), // Namazu chonk + new Group(6182, 1, 6182, 1, 0000, 0, 0000, 0, 0000, 0), // Imp + new Group(6147, 1, 6147, 1, 6147, 1, 6147, 1, 6147, 1) // Tonberry ); private FunEquipSet(params Group[] groups) diff --git a/Glamourer/State/FunModule.cs b/Glamourer/State/FunModule.cs index 14fc65a..7ddc42f 100644 --- a/Glamourer/State/FunModule.cs +++ b/Glamourer/State/FunModule.cs @@ -116,6 +116,7 @@ public unsafe class FunModule : IDisposable SetRandomItem(slot, ref armor); break; case CodeService.CodeFlag.Elephants: + case CodeService.CodeFlag.Dolphins: case CodeService.CodeFlag.World when actor.Index != 0: KeepOldArmor(actor, slot, ref armor); break; @@ -168,6 +169,10 @@ public unsafe class FunModule : IDisposable SetElephant(EquipSlot.Body, ref armor[1], stainId); SetElephant(EquipSlot.Head, ref armor[0], stainId); break; + case CodeService.CodeFlag.Dolphins: + SetDolphin(EquipSlot.Body, ref armor[1]); + SetDolphin(EquipSlot.Head, ref armor[0]); + break; case CodeService.CodeFlag.World when actor.Index != 0: _worldSets.Apply(actor, _rng, armor); break; @@ -227,6 +232,32 @@ public unsafe class FunModule : IDisposable 7, // Rose Pink ]; + private static IReadOnlyList DolphinBodies + => + [ + new CharacterArmor(6089, 1, 4), // Toad + new CharacterArmor(6089, 1, 4), // Toad + new CharacterArmor(6089, 1, 4), // Toad + new CharacterArmor(6023, 1, 4), // Swine + new CharacterArmor(6023, 1, 4), // Swine + new CharacterArmor(6023, 1, 4), // Swine + new CharacterArmor(6133, 1, 4), // Gaja + new CharacterArmor(6182, 1, 3), // Imp + new CharacterArmor(6182, 1, 3), // Imp + new CharacterArmor(6182, 1, 4), // Imp + new CharacterArmor(6182, 1, 4), // Imp + ]; + + private void SetDolphin(EquipSlot slot, ref CharacterArmor armor) + { + armor = slot switch + { + EquipSlot.Body => DolphinBodies[_rng.Next(0, DolphinBodies.Count - 1)], + EquipSlot.Head => new CharacterArmor(5040, 1, 0), + _ => armor, + }; + } + private void SetElephant(EquipSlot slot, ref CharacterArmor armor, StainId stainId) { armor = slot switch From b5b9289dc2c1b19cbf9fec7a095ec35a6d230050 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 6 Feb 2024 16:42:43 +0100 Subject: [PATCH 16/17] Change mousewheel to ctrl, current material state. --- Glamourer/Designs/Design.cs | 2 + Glamourer/Designs/DesignBase.cs | 50 +++- Glamourer/Designs/DesignConverter.cs | 38 ++- Glamourer/Designs/DesignEditor.cs | 9 + Glamourer/GameData/CustomizeParameterValue.cs | 7 + .../CustomizationDrawer.Simple.cs | 2 +- .../Gui/Equipment/GlamourerColorCombo.cs | 2 +- Glamourer/Gui/Equipment/ItemCombo.cs | 2 +- Glamourer/Gui/Equipment/WeaponCombo.cs | 2 +- Glamourer/Gui/Materials/MaterialDrawer.cs | 149 ++++------- Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 2 +- .../Gui/Tabs/DesignTab/DesignColorCombo.cs | 2 +- Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 20 ++ Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs | 12 +- Glamourer/Interop/Material/MaterialManager.cs | 104 ++++++-- .../Interop/Material/MaterialValueIndex.cs | 148 ++--------- .../Interop/Material/MaterialValueManager.cs | 237 +++++++++++++++++- Glamourer/State/InternalStateEditor.cs | 10 +- Glamourer/State/StateApplier.cs | 13 +- Glamourer/State/StateEditor.cs | 22 +- 20 files changed, 537 insertions(+), 296 deletions(-) diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 20d85e7..4e1e72e 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -72,6 +72,7 @@ public sealed class Design : DesignBase, ISavable ["Equipment"] = SerializeEquipment(), ["Customize"] = SerializeCustomize(), ["Parameters"] = SerializeParameters(), + ["Materials"] = SerializeMaterials(), ["Mods"] = SerializeMods(), ["Links"] = Links.Serialize(), }; @@ -136,6 +137,7 @@ public sealed class Design : DesignBase, ISavable LoadEquip(items, json["Equipment"], design, design.Name, true); LoadMods(json["Mods"], design); LoadParameters(json["Parameters"], design, design.Name); + LoadMaterials(json["Materials"], design, design.Name); LoadLinks(linkLoader, json["Links"], design); design.Color = json["Color"]?.ToObject() ?? string.Empty; return design; diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index 9699bc2..8873339 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -14,7 +14,7 @@ public class DesignBase { public const int FileVersion = 1; - private DesignData _designData = new(); + private DesignData _designData = new(); private readonly DesignMaterialManager _materials = new(); /// For read-only information about custom material color changes. @@ -86,9 +86,9 @@ public class DesignBase internal CustomizeFlag ApplyCustomizeRaw => _applyCustomize; - internal EquipFlag ApplyEquip = EquipFlagExtensions.All; - internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; - internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; + internal EquipFlag ApplyEquip = EquipFlagExtensions.All; + internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; + internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState; private bool _writeProtected; public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize) @@ -124,7 +124,6 @@ public class DesignBase _writeProtected = value; return true; - } public bool DoApplyEquip(EquipSlot slot) @@ -244,6 +243,7 @@ public class DesignBase ["Equipment"] = SerializeEquipment(), ["Customize"] = SerializeCustomize(), ["Parameters"] = SerializeParameters(), + ["Materials"] = SerializeMaterials(), }; return ret; } @@ -362,6 +362,45 @@ public class DesignBase return ret; } + protected JObject SerializeMaterials() + { + var ret = new JObject(); + foreach (var (key, value) in Materials) + ret[key.ToString("X16")] = JToken.FromObject(value); + return ret; + } + + protected static void LoadMaterials(JToken? materials, DesignBase design, string name) + { + if (materials is not JObject obj) + return; + + design.GetMaterialDataRef().Clear(); + foreach (var (key, value) in obj.Properties().Zip(obj.PropertyValues())) + { + try + { + var k = uint.Parse(key.Name, NumberStyles.HexNumber); + var v = value.ToObject(); + if (!MaterialValueIndex.FromKey(k, out var idx)) + { + Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.", + NotificationType.Warning); + continue; + } + + if (!design.GetMaterialDataRef().TryAddValue(MaterialValueIndex.FromKey(k), v)) + Glamourer.Messager.NotificationMessage($"Duplicate material value key {k} for design {name}, skipped.", + NotificationType.Warning); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Error parsing material value for design {name}, skipped", + NotificationType.Warning); + } + } + } + #endregion #region Deserialization @@ -382,6 +421,7 @@ public class DesignBase LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true); LoadEquip(items, json["Equipment"], ret, "Temporary Design", true); LoadParameters(json["Parameters"], ret, "Temporary Design"); + LoadMaterials(json["Materials"], ret, "Temporary Design"); return ret; } diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs index be70672..9b9ebfc 100644 --- a/Glamourer/Designs/DesignConverter.cs +++ b/Glamourer/Designs/DesignConverter.cs @@ -1,4 +1,5 @@ using Glamourer.Designs.Links; +using Glamourer.Interop.Material; using Glamourer.Services; using Glamourer.State; using Glamourer.Utility; @@ -6,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; using Penumbra.GameData.Structs; namespace Glamourer.Designs; @@ -38,22 +40,23 @@ public class DesignConverter( => ShareBase64(ShareJObject(design)); public string ShareBase64(ActorState state, in ApplicationRules rules) - => ShareBase64(state.ModelData, rules); + => ShareBase64(state.ModelData, state.Materials, rules); - public string ShareBase64(in DesignData data, in ApplicationRules rules) + public string ShareBase64(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules) { - var design = Convert(data, rules); + var design = Convert(data, materials, rules); return ShareBase64(ShareJObject(design)); } public DesignBase Convert(ActorState state, in ApplicationRules rules) - => Convert(state.ModelData, rules); + => Convert(state.ModelData, state.Materials, rules); - public DesignBase Convert(in DesignData data, in ApplicationRules rules) + public DesignBase Convert(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules) { var design = _designs.CreateTemporary(); rules.Apply(design); design.SetDesignData(_customize, data); + ComputeMaterials(design.GetMaterialDataRef(), materials, rules.Equip); return design; } @@ -181,4 +184,29 @@ public class DesignConverter( yield return (EquipSlot.OffHand, oh, offhand.Stain); } + + private static void ComputeMaterials(DesignMaterialManager manager, in StateMaterialManager materials, + EquipFlag equipFlags = EquipFlagExtensions.All) + { + foreach (var (key, value) in materials.Values) + { + var idx = MaterialValueIndex.FromKey(key); + if (idx.RowIndex >= MtrlFile.ColorTable.NumRows) + continue; + if (idx.MaterialIndex >= MaterialService.MaterialsPerModel) + continue; + + var slot = idx.DrawObject switch + { + MaterialValueIndex.DrawObjectType.Human => idx.SlotIndex < 10 ? ((uint)idx.SlotIndex).ToEquipSlot() : EquipSlot.Unknown, + MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0 => EquipSlot.MainHand, + MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0 => EquipSlot.OffHand, + _ => EquipSlot.Unknown, + }; + if (slot is EquipSlot.Unknown || (slot.ToBothFlags() & equipFlags) == 0) + continue; + + manager.AddOrUpdateValue(idx, value.Convert()); + } + } } diff --git a/Glamourer/Designs/DesignEditor.cs b/Glamourer/Designs/DesignEditor.cs index ab258d7..9640c71 100644 --- a/Glamourer/Designs/DesignEditor.cs +++ b/Glamourer/Designs/DesignEditor.cs @@ -1,6 +1,7 @@ using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; +using Glamourer.Interop.Material; using Glamourer.Services; using Glamourer.State; using Penumbra.GameData.Enums; @@ -250,6 +251,14 @@ public class DesignEditor( foreach (var parameter in CustomizeParameterExtensions.AllFlags.Where(other.DoApplyParameter)) ChangeCustomizeParameter(design, parameter, other.DesignData.Parameters[parameter]); + + foreach (var (key, value) in other.Materials) + { + if (!value.Enabled) + continue; + + design.GetMaterialDataRef().AddOrUpdateValue(MaterialValueIndex.FromKey(key), value); + } } /// Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. diff --git a/Glamourer/GameData/CustomizeParameterValue.cs b/Glamourer/GameData/CustomizeParameterValue.cs index 3bfdf99..87ab851 100644 --- a/Glamourer/GameData/CustomizeParameterValue.cs +++ b/Glamourer/GameData/CustomizeParameterValue.cs @@ -62,4 +62,11 @@ public static class VectorExtensions [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static bool NearEqual(this CustomizeParameterValue lhs, CustomizeParameterValue rhs, float eps = 1e-9f) => NearEqual(lhs.InternalQuadruple, rhs.InternalQuadruple, eps); + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static bool NearEqual(this float lhs, float rhs, float eps = 1e-5f) + { + var diff = lhs - rhs; + return diff < 0 ? diff > -eps : diff < eps; + } } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs index 3a8e021..e684554 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -207,7 +207,7 @@ public partial class CustomizationDrawer private static bool CaptureMouseWheel(ref int value, int offset, int cap) { - if (!ImGui.IsItemHovered()) + if (!ImGui.IsItemHovered() || !ImGui.GetIO().KeyCtrl) return false; ImGuiInternal.ItemSetUsingMouseWheel(); diff --git a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs index 3a791d0..d3fde9f 100644 --- a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs +++ b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs @@ -10,7 +10,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Gui.Equipment; public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, FavoriteManager _favorites) - : FilterComboColors(_comboWidth, MouseWheelType.Unmodified, CreateFunc(_stains, _favorites), Glamourer.Log) + : FilterComboColors(_comboWidth, MouseWheelType.Control, CreateFunc(_stains, _favorites), Glamourer.Log) { protected override bool DrawSelectable(int globalIdx, bool selected) { diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/Glamourer/Gui/Equipment/ItemCombo.cs index a6f22b5..24ff582 100644 --- a/Glamourer/Gui/Equipment/ItemCombo.cs +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -24,7 +24,7 @@ public sealed class ItemCombo : FilterComboCache public Variant CustomVariant { get; private set; } public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites) - : base(() => GetItems(favorites, items, slot), MouseWheelType.Unmodified, log) + : base(() => GetItems(favorites, items, slot), MouseWheelType.Control, log) { _favorites = favorites; Label = GetLabel(gameData, slot); diff --git a/Glamourer/Gui/Equipment/WeaponCombo.cs b/Glamourer/Gui/Equipment/WeaponCombo.cs index b6c3218..17abb24 100644 --- a/Glamourer/Gui/Equipment/WeaponCombo.cs +++ b/Glamourer/Gui/Equipment/WeaponCombo.cs @@ -17,7 +17,7 @@ public sealed class WeaponCombo : FilterComboCache private float _innerWidth; public WeaponCombo(ItemManager items, FullEquipType type, Logger log) - : base(() => GetWeapons(items, type), MouseWheelType.Unmodified, log) + : base(() => GetWeapons(items, type), MouseWheelType.Control, log) { Label = GetLabel(type); SearchByParts = true; diff --git a/Glamourer/Gui/Materials/MaterialDrawer.cs b/Glamourer/Gui/Materials/MaterialDrawer.cs index e4a612d..fb6a4ac 100644 --- a/Glamourer/Gui/Materials/MaterialDrawer.cs +++ b/Glamourer/Gui/Materials/MaterialDrawer.cs @@ -1,18 +1,20 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Designs; using Glamourer.Interop.Material; using Glamourer.Interop.Structs; using Glamourer.State; using ImGuiNET; +using OtterGui; using OtterGui.Services; +using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Materials; -public unsafe class MaterialDrawer(StateManager _stateManager) : IService +public unsafe class MaterialDrawer(StateManager _stateManager, DesignManager _designManager) : IService { private static readonly IReadOnlyList Types = [ @@ -23,135 +25,94 @@ public unsafe class MaterialDrawer(StateManager _stateManager) : IService private ActorState? _state; - public void DrawPanel(Actor actor) + public void DrawActorPanel(Actor actor) { if (!actor.IsCharacter || !_stateManager.GetOrCreate(actor, out _state)) return; - foreach (var type in Types) - { - var index = new MaterialValueIndex(type, 0, 0, 0, 0); - if (index.TryGetModel(actor, out var model)) - DrawModelType(model, index); - } - } - - private void DrawModelType(Model model, MaterialValueIndex sourceIndex) - { - using var tree = ImRaii.TreeNode(sourceIndex.DrawObject.ToString()); - if (!tree) + var model = actor.Model; + if (!model.IsHuman) return; - var names = model.AsCharacterBase->GetModelType() is CharacterBase.ModelType.Human - ? SlotNamesHuman - : SlotNames; - for (byte i = 0; i < model.AsCharacterBase->SlotCount; ++i) - { - var index = sourceIndex with { SlotIndex = i }; - DrawSlot(model, names, index); - } - } - - private void DrawSlot(Model model, IReadOnlyList names, MaterialValueIndex sourceIndex) - { - using var tree = ImRaii.TreeNode(names[sourceIndex.SlotIndex]); - if (!tree) + if (model.AsCharacterBase->SlotCount < 10) return; - for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i) + // Humans should have at least 10 slots for the equipment types. Technically more. + foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex()) { - var index = sourceIndex with { MaterialIndex = i }; - var texture = model.AsCharacterBase->ColorTableTextures + index.SlotIndex * MaterialService.MaterialsPerModel + i; + var item = model.GetArmor(slot).ToWeapon(0); + DrawSlotMaterials(model, slot.ToName(), item, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Human, (byte) idx, 0, 0)); + } + + var (mainhand, offhand, mh, oh) = actor.Model.GetWeapons(actor); + if (mainhand.IsWeapon && mainhand.AsCharacterBase->SlotCount > 0) + DrawSlotMaterials(mainhand, EquipSlot.MainHand.ToName(), mh, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Mainhand, 0, 0, 0)); + if (offhand.IsWeapon && offhand.AsCharacterBase->SlotCount > 0) + DrawSlotMaterials(offhand, EquipSlot.OffHand.ToName(), oh, new MaterialValueIndex(MaterialValueIndex.DrawObjectType.Offhand, 0, 0, 0)); + } + + + private void DrawSlotMaterials(Model model, string name, CharacterWeapon drawData, MaterialValueIndex index) + { + var drawnMaterial = 1; + for (byte materialIndex = 0; materialIndex < MaterialService.MaterialsPerModel; ++materialIndex) + { + var texture = model.AsCharacterBase->ColorTableTextures + index.SlotIndex * MaterialService.MaterialsPerModel + materialIndex; if (*texture == null) continue; if (!DirectXTextureHelper.TryGetColorTable(*texture, out var table)) continue; - DrawMaterial(ref table, index); + using var tree = ImRaii.TreeNode($"{name} Material #{drawnMaterial++}###{name}{materialIndex}"); + if (!tree) + continue; + + DrawMaterial(ref table, drawData, index with { MaterialIndex = materialIndex} ); } } - private void DrawMaterial(ref MtrlFile.ColorTable table, MaterialValueIndex sourceIndex) + private void DrawMaterial(ref MtrlFile.ColorTable table, CharacterWeapon drawData, MaterialValueIndex sourceIndex) { - using var tree = ImRaii.TreeNode($"Material {sourceIndex.MaterialIndex + 1}"); - if (!tree) - return; - for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i) { var index = sourceIndex with { RowIndex = i }; ref var row = ref table[i]; - DrawRow(ref row, index); + DrawRow(ref row, drawData, index); } } - private void DrawRow(ref MtrlFile.ColorTable.Row row, MaterialValueIndex sourceIndex) + private void DrawRow(ref MtrlFile.ColorTable.Row row, CharacterWeapon drawData, MaterialValueIndex index) { - var r = _state!.Materials.GetValues( - MaterialValueIndex.Min(sourceIndex.DrawObject, sourceIndex.SlotIndex, sourceIndex.MaterialIndex, sourceIndex.RowIndex), - MaterialValueIndex.Max(sourceIndex.DrawObject, sourceIndex.SlotIndex, sourceIndex.MaterialIndex, sourceIndex.RowIndex)); - - var highlightColor = ColorId.FavoriteStarOn.Value(); - - using var id = ImRaii.PushId(sourceIndex.RowIndex); - var index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Diffuse }; - var (diffuse, diffuseGame, changed) = MaterialValueManager.GetSpecific(r, index, out var d) - ? (d.Model, d.Game, true) - : (row.Diffuse, row.Diffuse, false); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) + using var id = ImRaii.PushId(index.RowIndex); + var changed = _state!.Materials.TryGetValue(index, out var value); + if (!changed) { - if (ImGui.ColorEdit3("Diffuse", ref diffuse, ImGuiColorEditFlags.NoInputs)) - _stateManager.ChangeMaterialValue(_state!, index, diffuse, diffuseGame, ApplySettings.Manual); + var internalRow = new ColorRow(row); + value = new MaterialValueState(internalRow, internalRow, drawData, StateSource.Manual); } - - index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Specular }; - (var specular, var specularGame, changed) = MaterialValueManager.GetSpecific(r, index, out var s) - ? (s.Model, s.Game, true) - : (row.Specular, row.Specular, false); + var applied = ImGui.ColorEdit3("Diffuse", ref value.Model.Diffuse, ImGuiColorEditFlags.NoInputs); ImGui.SameLine(); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) - { - if (ImGui.ColorEdit3("Specular", ref specular, ImGuiColorEditFlags.NoInputs)) - _stateManager.ChangeMaterialValue(_state!, index, specular, specularGame, ApplySettings.Manual); - } - - index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.Emissive }; - (var emissive, var emissiveGame, changed) = MaterialValueManager.GetSpecific(r, index, out var e) - ? (e.Model, e.Game, true) - : (row.Emissive, row.Emissive, false); + applied |= ImGui.ColorEdit3("Specular", ref value.Model.Specular, ImGuiColorEditFlags.NoInputs); ImGui.SameLine(); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) - { - if (ImGui.ColorEdit3("Emissive", ref emissive, ImGuiColorEditFlags.NoInputs)) - _stateManager.ChangeMaterialValue(_state!, index, emissive, emissiveGame, ApplySettings.Manual); - } - - index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.GlossStrength }; - (var glossStrength, var glossStrengthGame, changed) = MaterialValueManager.GetSpecific(r, index, out var g) - ? (g.Model.X, g.Game.X, true) - : (row.GlossStrength, row.GlossStrength, false); + applied |= ImGui.ColorEdit3("Emissive", ref value.Model.Emissive, ImGuiColorEditFlags.NoInputs); ImGui.SameLine(); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) - { - if (ImGui.DragFloat("Gloss", ref glossStrength, 0.1f)) - _stateManager.ChangeMaterialValue(_state!, index, new Vector3(glossStrength), new Vector3(glossStrengthGame), - ApplySettings.Manual); - } - - index = sourceIndex with { DataIndex = MaterialValueIndex.ColorTableIndex.SpecularStrength }; - (var specularStrength, var specularStrengthGame, changed) = MaterialValueManager.GetSpecific(r, index, out var ss) - ? (ss.Model.X, ss.Game.X, true) - : (row.SpecularStrength, row.SpecularStrength, false); + applied |= ImGui.DragFloat("Gloss", ref value.Model.GlossStrength, 0.1f); ImGui.SameLine(); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - using (ImRaii.PushColor(ImGuiCol.Text, highlightColor, changed)) + applied |= ImGui.DragFloat("Specular Strength", ref value.Model.SpecularStrength, 0.1f); + if (applied) + _stateManager.ChangeMaterialValue(_state!, index, value, ApplySettings.Manual); + if (changed) { - if (ImGui.DragFloat("Specular Strength", ref specularStrength, 0.1f)) - _stateManager.ChangeMaterialValue(_state!, index, new Vector3(specularStrength), new Vector3(specularStrengthGame), - ApplySettings.Manual); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FavoriteStarOn.Value()); + ImGui.TextUnformatted(FontAwesomeIcon.UserEdit.ToIconString()); + } } } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index ff6b798..90ff4b7 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -119,7 +119,7 @@ public class ActorPanel( if (ImGui.CollapsingHeader("Material Shit")) - _materialDrawer.DrawPanel(_actor); + _materialDrawer.DrawActorPanel(_actor); using var disabled = ImRaii.Disabled(transformationId != 0); if (_state.ModelData.IsHuman) DrawHumanPanel(); diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs index 72d717c..e59be09 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs @@ -10,7 +10,7 @@ public sealed class DesignColorCombo(DesignColors _designColors, bool _skipAutom FilterComboCache(_skipAutomatic ? _designColors.Keys.OrderBy(k => k) : _designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName), - MouseWheelType.Shift, Glamourer.Log) + MouseWheelType.Control, Glamourer.Log) { protected override void OnMouseWheel(string preview, ref int current, int steps) { diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index a957975..13cd293 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -174,6 +174,25 @@ public class DesignPanel( _parameterDrawer.Draw(_manager, _selector.Selected!); } + private void DrawMaterialValues() + { + if (!_config.UseAdvancedParameters) + return; + + using var h = ImRaii.CollapsingHeader("Advanced Dyes"); + if (!h) + return; + + foreach (var ((key, value), i) in _selector.Selected!.Materials.WithIndex()) + { + using var id = ImRaii.PushId(i); + ImGui.TextUnformatted($"{key:X16}"); + ImGui.SameLine(); + var enabled = value.Enabled; + ImGui.Checkbox("Enabled", ref enabled); + } + } + private void DrawCustomizeApplication() { using var id = ImRaii.PushId("Customizations"); @@ -365,6 +384,7 @@ public class DesignPanel( DrawCustomize(); DrawEquipment(); DrawCustomizeParameters(); + DrawMaterialValues(); _designDetails.Draw(); DrawApplicationRules(); _modAssociations.Draw(); diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs index 77dafa9..eb61580 100644 --- a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs +++ b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs @@ -85,7 +85,7 @@ public class NpcPanel( try { var data = ToDesignData(); - var text = _converter.ShareBase64(data, ApplicationRules.NpcFromModifiers()); + var text = _converter.ShareBase64(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); ImGui.SetClipboardText(text); } catch (Exception ex) @@ -101,7 +101,7 @@ public class NpcPanel( ImGui.OpenPopup("Save as Design"); _newName = _selector.Selection.Name; var data = ToDesignData(); - _newDesign = _converter.Convert(data, ApplicationRules.NpcFromModifiers()); + _newDesign = _converter.Convert(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); } private void SaveDesignDrawPopup() @@ -195,7 +195,7 @@ public class NpcPanel( if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var design = _converter.Convert(ToDesignData(), ApplicationRules.NpcFromModifiers()); + var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); _state.ApplyDesign(state, design, ApplySettings.Manual); } } @@ -213,7 +213,7 @@ public class NpcPanel( if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var design = _converter.Convert(ToDesignData(), ApplicationRules.NpcFromModifiers()); + var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); _state.ApplyDesign(state, design, ApplySettings.Manual); } } @@ -247,7 +247,9 @@ public class NpcPanel( var colorName = color.Length == 0 ? DesignColors.AutomaticName : color; ImGui.TableNextColumn(); if (_colorCombo.Draw("##colorCombo", colorName, - "Associate a color with this NPC appearance. Right-Click to revert to automatic coloring.", + "Associate a color with this NPC appearance.\n" + + "Right-Click to revert to automatic coloring.\n" + + "Hold Control and scroll the mousewheel to scroll.", width - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight()) && _colorCombo.CurrentSelection != null) { diff --git a/Glamourer/Interop/Material/MaterialManager.cs b/Glamourer/Interop/Material/MaterialManager.cs index 8cbe0c5..f35ee8a 100644 --- a/Glamourer/Interop/Material/MaterialManager.cs +++ b/Glamourer/Interop/Material/MaterialManager.cs @@ -6,6 +6,8 @@ using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Services; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; using Penumbra.GameData.Structs; namespace Glamourer.Interop.Material; @@ -19,6 +21,8 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable private int _lastSlot; + private readonly ThreadLocal> _deleteList = new(() => []); + public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra) { _stateManager = stateManager; @@ -39,7 +43,8 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable var (slotId, materialId) = FindMaterial(characterBase, material); if (!validType - || slotId == byte.MaxValue + || slotId > 9 + || type is not MaterialValueIndex.DrawObjectType.Human && slotId > 0 || !actor.Identifier(_actors, out var identifier) || !_stateManager.TryGetValue(identifier, out var state)) return; @@ -53,38 +58,60 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable if (!PrepareColorSet.TryGetColorTable(characterBase, material, stain, out var baseColorSet)) return; - for (var i = 0; i < values.Length; ++i) + var drawData = type switch { - var idx = MaterialValueIndex.FromKey(values[i].key); - var (oldGame, model, source) = values[i].Value; - ref var row = ref baseColorSet[idx.RowIndex]; - if (!idx.DataIndex.TryGetValue(row, out var newGame)) - continue; - - if (newGame == oldGame) - { - idx.DataIndex.SetValue(ref row, model); - } - else - { - switch (source.Base()) - { - case StateSource.Manual: - _stateManager.ChangeMaterialValue(state, idx, Vector3.Zero, Vector3.Zero, ApplySettings.Game); - --i; - break; - case StateSource.Fixed: - idx.DataIndex.SetValue(ref row, model); - state.Materials.UpdateValue(idx, new MaterialValueState(newGame, model, source), out _); - break; - } - } - } + MaterialValueIndex.DrawObjectType.Human => GetTempSlot((Human*)characterBase, slotId), + _ => GetTempSlot((Weapon*)characterBase), + }; + UpdateMaterialValues(state, values, drawData, ref baseColorSet); if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture)) ret = (nint)texture; } + /// Update and apply the glamourer state of an actor according to the application sources when updated by the game. + private void UpdateMaterialValues(ActorState state, ReadOnlySpan<(uint Key, MaterialValueState Value)> values, CharacterWeapon drawData, + ref MtrlFile.ColorTable colorTable) + { + var deleteList = _deleteList.Value!; + deleteList.Clear(); + for (var i = 0; i < values.Length; ++i) + { + var idx = MaterialValueIndex.FromKey(values[i].Key); + var materialValue = values[i].Value; + ref var row = ref colorTable[idx.RowIndex]; + var newGame = new ColorRow(row); + if (materialValue.EqualGame(newGame, drawData)) + materialValue.Model.Apply(ref row); + else + switch (materialValue.Source) + { + case StateSource.Pending: + materialValue.Model.Apply(ref row); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, StateSource.Manual), + out _); + break; + case StateSource.IpcManual: + case StateSource.Manual: + deleteList.Add(idx); + break; + case StateSource.Fixed: + case StateSource.IpcFixed: + materialValue.Model.Apply(ref row); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, materialValue.Source), + out _); + break; + } + } + + foreach (var idx in deleteList) + _stateManager.ChangeMaterialValue(state, idx, default, ApplySettings.Game); + } + + /// + /// Find the index of a material by searching through a draw objects pointers. + /// Tries to take shortcuts for consecutive searches like when a character is newly created. + /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private (byte SlotId, byte MaterialId) FindMaterial(CharacterBase* characterBase, MaterialResourceHandle* material) { @@ -119,6 +146,7 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable return (byte.MaxValue, byte.MaxValue); } + /// Find the type of the given draw object by checking the actors pointers. private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type) { type = MaterialValueIndex.DrawObjectType.Human; @@ -145,4 +173,26 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable return false; } + + /// We need to get the temporary set, variant and stain that is currently being set if it is available. + private CharacterWeapon GetTempSlot(Human* human, byte slotId) + { + if (human->ChangedEquipData == null) + return ((Model)human).GetArmor(((uint)slotId).ToEquipSlot()).ToWeapon(0); + + return ((CharacterArmor*)human->ChangedEquipData + slotId * 3)->ToWeapon(0); + } + + /// + /// We need to get the temporary set, variant and stain that is currently being set if it is available. + /// Weapons do not change in skeleton id without being reconstructed, so this is not changeable data. + /// + private CharacterWeapon GetTempSlot(Weapon* weapon) + { + var changedData = *(void**)((byte*)weapon + 0x918); + if (changedData == null) + return new CharacterWeapon(weapon->ModelSetId, weapon->SecondaryId, (Variant)weapon->Variant, (StainId)weapon->ModelUnknown); + + return new CharacterWeapon(weapon->ModelSetId, *(SecondaryId*)changedData, ((Variant*)changedData)[2], ((StainId*)changedData)[3]); + } } diff --git a/Glamourer/Interop/Material/MaterialValueIndex.cs b/Glamourer/Interop/Material/MaterialValueIndex.cs index d2d8db1..47ddb7a 100644 --- a/Glamourer/Interop/Material/MaterialValueIndex.cs +++ b/Glamourer/Interop/Material/MaterialValueIndex.cs @@ -11,14 +11,13 @@ public readonly record struct MaterialValueIndex( MaterialValueIndex.DrawObjectType DrawObject, byte SlotIndex, byte MaterialIndex, - byte RowIndex, - MaterialValueIndex.ColorTableIndex DataIndex) + byte RowIndex) { public uint Key - => ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex, DataIndex); + => ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex); public bool Valid - => Validate(DrawObject) && ValidateSlot(SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex) && Validate(DataIndex); + => Validate(DrawObject) && ValidateSlot(SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex); public static bool FromKey(uint key, out MaterialValueIndex index) { @@ -96,7 +95,7 @@ public readonly record struct MaterialValueIndex( public unsafe bool TryGetColorTable(Texture** texture, out MtrlFile.ColorTable table) => DirectXTextureHelper.TryGetColorTable(*texture, out table); - public unsafe bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row) + public bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row) { if (!TryGetColorTable(actor, out var table)) { @@ -108,40 +107,16 @@ public readonly record struct MaterialValueIndex( 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 Min(DrawObjectType drawObject = 0, byte slotIndex = 0, byte materialIndex = 0, byte rowIndex = 0) + => new(drawObject, slotIndex, materialIndex, rowIndex); 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); + byte materialIndex = byte.MaxValue, byte rowIndex = byte.MaxValue) + => new(drawObject, slotIndex, materialIndex, rowIndex); public enum DrawObjectType : byte { @@ -150,18 +125,6 @@ public readonly record struct MaterialValueIndex( Offhand, }; - public enum ColorTableIndex : byte - { - Diffuse, - Specular, - SpecularStrength, - Emissive, - GlossStrength, - TileSet, - MaterialRepeat, - MaterialSkew, - } - public static bool Validate(DrawObjectType type) => Enum.IsDefined(type); @@ -174,22 +137,17 @@ public readonly record struct MaterialValueIndex( 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) + private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex) { - 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; + var result = (uint)rowIndex; + result |= (uint)materialIndex << 8; + result |= (uint)slotIndex << 16; + result |= (uint)((byte)type << 24); return result; } private MaterialValueIndex(uint key) - : this((DrawObjectType)((key >> 28) & 0xF), (byte)(key >> 20), (byte)((key >> 16) & 0xF), (byte)(key >> 8), - (ColorTableIndex)(key & 0xFF)) + : this((DrawObjectType)(key >> 24), (byte)(key >> 16), (byte)(key >> 8), (byte)key) { } private class Converter : JsonConverter @@ -202,81 +160,3 @@ public readonly record struct MaterialValueIndex( => 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 index 5f7c2dc..c735182 100644 --- a/Glamourer/Interop/Material/MaterialValueManager.cs +++ b/Glamourer/Interop/Material/MaterialValueManager.cs @@ -1,12 +1,242 @@ global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager; global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager; +using Glamourer.GameData; using Glamourer.State; +using Penumbra.GameData.Files; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; namespace Glamourer.Interop.Material; -public record struct MaterialValueDesign(Vector3 Value, bool Enabled); -public record struct MaterialValueState(Vector3 Game, Vector3 Model, StateSource Source); +[JsonConverter(typeof(Converter))] +public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, float specularStrength, float glossStrength) +{ + public static readonly ColorRow Empty = new(Vector3.Zero, Vector3.Zero, Vector3.Zero, 0, 0); + + public Vector3 Diffuse = diffuse; + public Vector3 Specular = specular; + public Vector3 Emissive = emissive; + public float SpecularStrength = specularStrength; + public float GlossStrength = glossStrength; + + public ColorRow(in MtrlFile.ColorTable.Row row) + : this(row.Diffuse, row.Specular, row.Emissive, row.SpecularStrength, row.GlossStrength) + { } + + public readonly bool NearEqual(in ColorRow rhs) + => Diffuse.NearEqual(rhs.Diffuse) + && Specular.NearEqual(rhs.Specular) + && Emissive.NearEqual(rhs.Emissive) + && SpecularStrength.NearEqual(rhs.SpecularStrength) + && GlossStrength.NearEqual(rhs.GlossStrength); + + public readonly bool Apply(ref MtrlFile.ColorTable.Row row) + { + var ret = false; + if (!row.Diffuse.NearEqual(Diffuse)) + { + row.Diffuse = Diffuse; + ret = true; + } + + if (!row.Specular.NearEqual(Specular)) + { + row.Specular = Specular; + ret = true; + } + + if (!row.Emissive.NearEqual(Emissive)) + { + row.Emissive = Emissive; + ret = true; + } + + if (!row.SpecularStrength.NearEqual(SpecularStrength)) + { + row.SpecularStrength = SpecularStrength; + ret = true; + } + + if (!row.GlossStrength.NearEqual(GlossStrength)) + { + row.GlossStrength = GlossStrength; + ret = true; + } + + return ret; + } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ColorRow value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("DiffuseR"); + writer.WriteValue(value.Diffuse.X); + writer.WritePropertyName("DiffuseG"); + writer.WriteValue(value.Diffuse.Y); + writer.WritePropertyName("DiffuseB"); + writer.WriteValue(value.Diffuse.Z); + writer.WritePropertyName("SpecularR"); + writer.WriteValue(value.Specular.X); + writer.WritePropertyName("SpecularG"); + writer.WriteValue(value.Specular.Y); + writer.WritePropertyName("SpecularB"); + writer.WriteValue(value.Specular.Z); + writer.WritePropertyName("SpecularA"); + writer.WriteValue(value.SpecularStrength); + writer.WritePropertyName("EmissiveR"); + writer.WriteValue(value.Emissive.X); + writer.WritePropertyName("EmissiveG"); + writer.WriteValue(value.Emissive.Y); + writer.WritePropertyName("EmissiveB"); + writer.WriteValue(value.Emissive.Z); + writer.WritePropertyName("Gloss"); + writer.WriteValue(value.GlossStrength); + writer.WriteEndObject(); + } + + public override ColorRow ReadJson(JsonReader reader, Type objectType, ColorRow existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var obj = JObject.Load(reader); + Set(ref existingValue.Diffuse.X, obj["DiffuseR"]?.Value()); + Set(ref existingValue.Diffuse.Y, obj["DiffuseG"]?.Value()); + Set(ref existingValue.Diffuse.Z, obj["DiffuseB"]?.Value()); + Set(ref existingValue.Specular.X, obj["SpecularR"]?.Value()); + Set(ref existingValue.Specular.Y, obj["SpecularG"]?.Value()); + Set(ref existingValue.Specular.Z, obj["SpecularB"]?.Value()); + Set(ref existingValue.SpecularStrength, obj["SpecularA"]?.Value()); + Set(ref existingValue.Emissive.X, obj["EmissiveR"]?.Value()); + Set(ref existingValue.Emissive.Y, obj["EmissiveG"]?.Value()); + Set(ref existingValue.Emissive.Z, obj["EmissiveB"]?.Value()); + Set(ref existingValue.GlossStrength, obj["Gloss"]?.Value()); + return existingValue; + + static void Set(ref T target, T? value) + where T : struct + { + if (value.HasValue) + target = value.Value; + } + } + } +} + +[JsonConverter(typeof(Converter))] +public struct MaterialValueDesign(ColorRow value, bool enabled) +{ + public ColorRow Value = value; + public bool Enabled = enabled; + + public readonly bool Apply(ref MaterialValueState state) + { + if (!Enabled) + return false; + + if (state.Model.NearEqual(Value)) + return false; + + state.Model = Value; + return true; + } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MaterialValueDesign value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("DiffuseR"); + writer.WriteValue(value.Value.Diffuse.X); + writer.WritePropertyName("DiffuseG"); + writer.WriteValue(value.Value.Diffuse.Y); + writer.WritePropertyName("DiffuseB"); + writer.WriteValue(value.Value.Diffuse.Z); + writer.WritePropertyName("SpecularR"); + writer.WriteValue(value.Value.Specular.X); + writer.WritePropertyName("SpecularG"); + writer.WriteValue(value.Value.Specular.Y); + writer.WritePropertyName("SpecularB"); + writer.WriteValue(value.Value.Specular.Z); + writer.WritePropertyName("SpecularA"); + writer.WriteValue(value.Value.SpecularStrength); + writer.WritePropertyName("EmissiveR"); + writer.WriteValue(value.Value.Emissive.X); + writer.WritePropertyName("EmissiveG"); + writer.WriteValue(value.Value.Emissive.Y); + writer.WritePropertyName("EmissiveB"); + writer.WriteValue(value.Value.Emissive.Z); + writer.WritePropertyName("Gloss"); + writer.WriteValue(value.Value.GlossStrength); + writer.WritePropertyName("Enabled"); + writer.WriteValue(value.Enabled); + writer.WriteEndObject(); + } + + public override MaterialValueDesign ReadJson(JsonReader reader, Type objectType, MaterialValueDesign existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + var obj = JObject.Load(reader); + Set(ref existingValue.Value.Diffuse.X, obj["DiffuseR"]?.Value()); + Set(ref existingValue.Value.Diffuse.Y, obj["DiffuseG"]?.Value()); + Set(ref existingValue.Value.Diffuse.Z, obj["DiffuseB"]?.Value()); + Set(ref existingValue.Value.Specular.X, obj["SpecularR"]?.Value()); + Set(ref existingValue.Value.Specular.Y, obj["SpecularG"]?.Value()); + Set(ref existingValue.Value.Specular.Z, obj["SpecularB"]?.Value()); + Set(ref existingValue.Value.SpecularStrength, obj["SpecularA"]?.Value()); + Set(ref existingValue.Value.Emissive.X, obj["EmissiveR"]?.Value()); + Set(ref existingValue.Value.Emissive.Y, obj["EmissiveG"]?.Value()); + Set(ref existingValue.Value.Emissive.Z, obj["EmissiveB"]?.Value()); + Set(ref existingValue.Value.GlossStrength, obj["Gloss"]?.Value()); + existingValue.Enabled = obj["Enabled"]?.Value() ?? false; + return existingValue; + + static void Set(ref T target, T? value) + where T : struct + { + if (value.HasValue) + target = value.Value; + } + } + } +} + +[StructLayout(LayoutKind.Explicit)] +public struct MaterialValueState( + in ColorRow game, + in ColorRow model, + CharacterWeapon drawData, + StateSource source) +{ + public MaterialValueState(in ColorRow gameRow, in ColorRow modelRow, CharacterArmor armor, StateSource source) + : this(gameRow, modelRow, armor.ToWeapon(0), source) + { } + + [FieldOffset(0)] + public ColorRow Game = game; + + [FieldOffset(44)] + public ColorRow Model = model; + + [FieldOffset(88)] + public readonly CharacterWeapon DrawData = drawData; + + [FieldOffset(95)] + public readonly StateSource Source = source; + + public readonly bool EqualGame(in ColorRow rhsRow, CharacterWeapon rhsData) + => DrawData.Skeleton == rhsData.Skeleton + && DrawData.Weapon == rhsData.Weapon + && DrawData.Variant == rhsData.Variant + && DrawData.Stain == rhsData.Stain + && Game.NearEqual(rhsRow); + + public readonly MaterialValueDesign Convert() + => new(Model, true); +} public readonly struct MaterialValueManager { @@ -113,7 +343,7 @@ public readonly struct MaterialValueManager return count; } - public ReadOnlySpan<(uint key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) + public ReadOnlySpan<(uint Key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) => MaterialValueManager.Filter(CollectionsMarshal.AsSpan(_values), min, max); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -187,6 +417,7 @@ public static class MaterialValueManager maxIdx = ~maxIdx + idx; return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1); } + maxIdx += idx; while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey) diff --git a/Glamourer/State/InternalStateEditor.cs b/Glamourer/State/InternalStateEditor.cs index 75cee46..35587fe 100644 --- a/Glamourer/State/InternalStateEditor.cs +++ b/Glamourer/State/InternalStateEditor.cs @@ -222,7 +222,7 @@ public class InternalStateEditor( } /// Change the value of a single material color table entry. - public bool ChangeMaterialValue(ActorState state, MaterialValueIndex index, Vector3 value, Vector3 gameValue, StateSource source, out Vector3 oldValue, + public bool ChangeMaterialValue(ActorState state, MaterialValueIndex index, in MaterialValueState newValue, StateSource source, out ColorRow? oldValue, uint key = 0) { // We already have an existing value. @@ -240,18 +240,18 @@ public class InternalStateEditor( } // Update if edited. - state.Materials.UpdateValue(index, new MaterialValueState(gameValue, value, source), out _); + state.Materials.UpdateValue(index, newValue, out _); return true; } // We do not have an existing value. - oldValue = gameValue; + oldValue = null; // Do not do anything if locked or if the game value updates, because then we do not need to add an entry. if (!state.CanUnlock(key) || source is StateSource.Game) return false; - // Only add an entry if it is sufficiently different from the game value. - return !value.NearEqual(gameValue) && state.Materials.TryAddValue(index, new MaterialValueState(gameValue, value, source)); + // Only add an entry if it is different from the game value. + return state.Materials.TryAddValue(index, newValue); } public bool ChangeMetaState(ActorState state, MetaIndex index, bool value, StateSource source, out bool oldValue, diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index 19c1f3e..7e38726 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -276,7 +276,7 @@ public class StateApplier( return data; } - public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, Vector3? value, bool force) + public unsafe void ChangeMaterialValue(ActorData data, MaterialValueIndex index, ColorRow? value, bool force) { if (!force && !_config.UseAdvancedParameters) return; @@ -289,14 +289,11 @@ public class StateApplier( if (!index.TryGetColorTable(texture, out var table)) continue; - Vector3 actualValue; if (value.HasValue) - actualValue = value.Value; - else if (!PrepareColorSet.TryGetColorTable(actor, index, out var baseTable) - || !index.DataIndex.TryGetValue(baseTable[index.RowIndex], out actualValue)) - continue; - - if (!index.DataIndex.SetValue(ref table[index.RowIndex], actualValue)) + value.Value.Apply(ref table[index.RowIndex]); + else if (PrepareColorSet.TryGetColorTable(actor, index, out var baseTable)) + table[index.RowIndex] = baseTable[index.RowIndex]; + else continue; MaterialService.ReplaceColorTable(texture, table); diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index e747448..7d0c203 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -165,15 +165,15 @@ public class StateEditor( StateChanged.Invoke(StateChanged.Type.Parameter, settings.Source, state, actors, (old, @new, flag)); } - public void ChangeMaterialValue(object data, MaterialValueIndex index, Vector3 value, Vector3 gameValue, ApplySettings settings) + public void ChangeMaterialValue(object data, MaterialValueIndex index, in MaterialValueState newValue, ApplySettings settings) { var state = (ActorState)data; - if (!Editor.ChangeMaterialValue(state, index, value, gameValue, settings.Source, out var oldValue, settings.Key)) + if (!Editor.ChangeMaterialValue(state, index, newValue, settings.Source, out var oldValue, settings.Key)) return; var actors = Applier.ChangeMaterialValue(state, index, settings.Source.RequiresChange()); - Glamourer.Log.Verbose($"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChanged.Type.MaterialValue, settings.Source, state, actors, (oldValue, value, index)); + Glamourer.Log.Verbose($"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {newValue.Game}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChanged.Type.MaterialValue, settings.Source, state, actors, (oldValue, newValue.Game, index)); } /// @@ -282,6 +282,20 @@ public class StateEditor( if (!settings.RespectManual || !state.Sources[meta].IsManual()) Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key); } + + foreach (var (key, value) in mergedDesign.Design.Materials) + { + if (!value.Enabled) + continue; + + var idx = MaterialValueIndex.FromKey(key); + // TODO + //if (state.Materials.TryGetValue(idx, out var materialState)) + //{ + // if (!settings.RespectManual || materialState.Source.IsManual()) + // Editor.ChangeMaterialValue(state, idx, new MaterialValueState(materialState.Game, value.Value, materialState.DrawData)); + //} + } } var actors = settings.Source.RequiresChange() From 99181d2fdba92d18b2e08cc0a7ef0d1e33e8d05f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 6 Feb 2024 16:51:12 +0100 Subject: [PATCH 17/17] Disable Material stuff for now. --- Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 6 +++--- Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 2 +- Glamourer/Interop/Material/MaterialManager.cs | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 90ff4b7..fc69095 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -117,9 +117,9 @@ public class ActorPanel( RevertButtons(); - - if (ImGui.CollapsingHeader("Material Shit")) - _materialDrawer.DrawActorPanel(_actor); + // TODO Materials + //if (ImGui.CollapsingHeader("Material Shit")) + // _materialDrawer.DrawActorPanel(_actor); using var disabled = ImRaii.Disabled(transformationId != 0); if (_state.ModelData.IsHuman) DrawHumanPanel(); diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index 13cd293..4fa302e 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -384,7 +384,7 @@ public class DesignPanel( DrawCustomize(); DrawEquipment(); DrawCustomizeParameters(); - DrawMaterialValues(); + //DrawMaterialValues(); TODO Materials _designDetails.Draw(); DrawApplicationRules(); _modAssociations.Draw(); diff --git a/Glamourer/Interop/Material/MaterialManager.cs b/Glamourer/Interop/Material/MaterialManager.cs index f35ee8a..b8c33c8 100644 --- a/Glamourer/Interop/Material/MaterialManager.cs +++ b/Glamourer/Interop/Material/MaterialManager.cs @@ -30,7 +30,8 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable _penumbra = penumbra; _event = prepareColorSet; - _event.Subscribe(OnPrepareColorSet, PrepareColorSet.Priority.MaterialManager); + // TODO Material + //_event.Subscribe(OnPrepareColorSet, PrepareColorSet.Priority.MaterialManager); } public void Dispose()