From d8ce81cdc4fede559be5a1c1277c1cf49cb95c98 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 Feb 2024 18:10:45 +0100 Subject: [PATCH] Run auto redraw on framework, add some locks, handle material value application differently for ApplyAll. --- Glamourer/Gui/Materials/AdvancedDyePopup.cs | 17 +- Glamourer/Interop/Material/DirectXService.cs | 187 ++++++++++++++++++ .../Interop/Material/DirectXTextureHelper.cs | 116 ----------- .../Material/LiveColorTablePreviewer.cs | 16 +- Glamourer/Interop/Material/MaterialService.cs | 28 --- .../Interop/Material/MaterialValueIndex.cs | 25 --- .../Interop/Penumbra/PenumbraAutoRedraw.cs | 48 +++-- Glamourer/State/StateApplier.cs | 63 ++++-- Glamourer/State/StateEditor.cs | 2 + Glamourer/State/StateManager.cs | 9 +- 10 files changed, 287 insertions(+), 224 deletions(-) create mode 100644 Glamourer/Interop/Material/DirectXService.cs delete mode 100644 Glamourer/Interop/Material/DirectXTextureHelper.cs diff --git a/Glamourer/Gui/Materials/AdvancedDyePopup.cs b/Glamourer/Gui/Materials/AdvancedDyePopup.cs index f91f352..f863e1f 100644 --- a/Glamourer/Gui/Materials/AdvancedDyePopup.cs +++ b/Glamourer/Gui/Materials/AdvancedDyePopup.cs @@ -20,13 +20,14 @@ namespace Glamourer.Gui.Materials; public sealed unsafe class AdvancedDyePopup( Configuration config, StateManager stateManager, - LiveColorTablePreviewer preview) : IService + LiveColorTablePreviewer preview, + DirectXService directX) : IService { private MaterialValueIndex? _drawIndex; private ActorState _state = null!; private Actor _actor; private byte _selectedMaterial = byte.MaxValue; - private bool _anyChanged = false; + private bool _anyChanged; private bool ShouldBeDrawn() { @@ -94,7 +95,7 @@ public sealed unsafe class AdvancedDyePopup( for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i) { var index = _drawIndex!.Value with { MaterialIndex = i }; - var available = index.TryGetTexture(textures, out var texture) && index.TryGetColorTable(texture, out var table); + var available = index.TryGetTexture(textures, out var texture) && directX.TryGetColorTable(*texture, out var table); if (index == preview.LastValueIndex with { RowIndex = 0 }) table = preview.LastOriginalColorTable; @@ -179,7 +180,7 @@ public sealed unsafe class AdvancedDyePopup( } } - public unsafe void Draw(Actor actor, ActorState state) + public void Draw(Actor actor, ActorState state) { _actor = actor; _state = state; @@ -236,20 +237,20 @@ public sealed unsafe class AdvancedDyePopup( ? _state.ModelData.Weapon(slot) : _state.ModelData.Armor(slot).ToWeapon(0); var value = new MaterialValueState(internalRow, internalRow, weapon, StateSource.Manual); - stateManager.ChangeMaterialValue(_state!, materialIndex with { RowIndex = (byte)idx }, value, ApplySettings.Manual); + stateManager.ChangeMaterialValue(_state, materialIndex with { RowIndex = (byte)idx }, value, ApplySettings.Manual); } ImGui.SameLine(0, spacing); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UndoAlt.ToIconString(), buttonSize, "Reset this table to game state.", !_anyChanged, true)) for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i) - stateManager.ResetMaterialValue(_state, materialIndex with { RowIndex = (byte)i }, ApplySettings.Game); + stateManager.ResetMaterialValue(_state, materialIndex with { RowIndex = i }, ApplySettings.Game); } private void DrawRow(ref MtrlFile.ColorTable.Row row, MaterialValueIndex index, in MtrlFile.ColorTable table) { using var id = ImRaii.PushId(index.RowIndex); - var changed = _state!.Materials.TryGetValue(index, out var value); + var changed = _state.Materials.TryGetValue(index, out var value); if (!changed) { var internalRow = new ColorRow(row); @@ -314,7 +315,7 @@ public sealed unsafe class AdvancedDyePopup( stateManager.ResetMaterialValue(_state, index, ApplySettings.Game); if (applied) - stateManager.ChangeMaterialValue(_state!, index, value, ApplySettings.Manual); + stateManager.ChangeMaterialValue(_state, index, value, ApplySettings.Manual); } private LabelStruct _label = new(); diff --git a/Glamourer/Interop/Material/DirectXService.cs b/Glamourer/Interop/Material/DirectXService.cs new file mode 100644 index 0000000..b204dd6 --- /dev/null +++ b/Glamourer/Interop/Material/DirectXService.cs @@ -0,0 +1,187 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using Lumina.Data.Files; +using OtterGui.Services; +using Penumbra.String.Functions; +using SharpGen.Runtime; +using Vortice.Direct3D11; +using Vortice.DXGI; +using static Penumbra.GameData.Files.MtrlFile; +using MapFlags = Vortice.Direct3D11.MapFlags; +using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; + +namespace Glamourer.Interop.Material; + +public unsafe class DirectXService(IFramework framework) : IService +{ + private readonly object _lock = new(); + + private readonly ConcurrentDictionary _textures = []; + + /// 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 bool ReplaceColorTable(Texture** original, in ColorTable colorTable) + { + if (original == null) + return false; + + var textureSize = stackalloc int[2]; + textureSize[0] = MaterialService.TextureWidth; + textureSize[1] = MaterialService.TextureHeight; + + lock (_lock) + { + 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; + } + + Glamourer.Log.Verbose($"[{Thread.CurrentThread.ManagedThreadId}] Replaced texture {(ulong)*original:X} with new ColorTable."); + texture.Exchange(ref *(nint*)original); + } + + return true; + } + + public bool TryGetColorTable(Texture* texture, out ColorTable table) + { + if (_textures.TryGetValue((nint)texture, out var p) && framework.LastUpdateUTC == p.Update) + { + table = p.Table; + return true; + } + + lock (_lock) + { + if (!TextureColorTable(texture, out table)) + return false; + } + + _textures[(nint)texture] = (framework.LastUpdateUTC, table); + return true; + } + + /// 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. + private static bool TextureColorTable(Texture* texture, out 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, + }; + + var ret = resource.Device.As().CreateTexture2D1(desc); + Glamourer.Log.Excessive( + $"[{Thread.CurrentThread.ManagedThreadId}] Cloning resource {resource.NativePointer:X} to {ret.NativePointer:X}"); + return ret; + } + + /// Turn a mapped texture into a color table. + private static 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 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(ColorTable) != expectedSize || height != MaterialService.TextureHeight) + return default; + + var ret = new 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); + Glamourer.Log.Excessive( + $"[{Thread.CurrentThread.ManagedThreadId}] Copied resource data {res.NativePointer:X} to {stagingRes.NativePointer:X}"); + stagingRes.Device.ImmediateContext.Map(stagingRes, 0, MapMode.Read, MapFlags.None, out var mapInfo).CheckError(); + Glamourer.Log.Excessive( + $"[{Thread.CurrentThread.ManagedThreadId}] Mapped resource data for {stagingRes.NativePointer:X} to {mapInfo.DataPointer:X}"); + + try + { + return getData(stagingRes, mapInfo); + } + finally + { + Glamourer.Log.Excessive($"[{Thread.CurrentThread.ManagedThreadId}] Obtained resource data."); + stagingRes.Device.ImmediateContext.Unmap(stagingRes, 0); + Glamourer.Log.Excessive($"[{Thread.CurrentThread.ManagedThreadId}] Unmapped resource data for {stagingRes.NativePointer:X}"); + } + } + + private static readonly Result WasStillDrawing = new(0x887A000A); +} diff --git a/Glamourer/Interop/Material/DirectXTextureHelper.cs b/Glamourer/Interop/Material/DirectXTextureHelper.cs deleted file mode 100644 index 9932abc..0000000 --- a/Glamourer/Interop/Material/DirectXTextureHelper.cs +++ /dev/null @@ -1,116 +0,0 @@ -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/LiveColorTablePreviewer.cs b/Glamourer/Interop/Material/LiveColorTablePreviewer.cs index 8cd2b78..6ec3496 100644 --- a/Glamourer/Interop/Material/LiveColorTablePreviewer.cs +++ b/Glamourer/Interop/Material/LiveColorTablePreviewer.cs @@ -9,8 +9,9 @@ namespace Glamourer.Interop.Material; public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable { - private readonly IObjectTable _objects; - private readonly IFramework _framework; + private readonly IObjectTable _objects; + private readonly IFramework _framework; + private readonly DirectXService _directXService; public MaterialValueIndex LastValueIndex { get; private set; } = MaterialValueIndex.Invalid; public MtrlFile.ColorTable LastOriginalColorTable { get; private set; } @@ -19,11 +20,11 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable private ObjectIndex _objectIndex = ObjectIndex.AnyIndex; private MtrlFile.ColorTable _originalColorTable; - - public LiveColorTablePreviewer(IObjectTable objects, IFramework framework) + public LiveColorTablePreviewer(IObjectTable objects, IFramework framework, DirectXService directXService) { _objects = objects; _framework = framework; + _directXService = directXService; _framework.Update += OnFramework; } @@ -34,7 +35,7 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable var actor = (Actor)_objects.GetObjectAddress(_lastObjectIndex.Index); if (actor.IsCharacter && LastValueIndex.TryGetTexture(actor, out var texture)) - MaterialService.ReplaceColorTable(texture, LastOriginalColorTable); + _directXService.ReplaceColorTable(texture, LastOriginalColorTable); LastValueIndex = MaterialValueIndex.Invalid; _lastObjectIndex = ObjectIndex.AnyIndex; @@ -78,15 +79,14 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable } else { - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) { - table[i].Diffuse = diffuse; + table[i].Diffuse = diffuse; table[i].Emissive = emissive; } } - MaterialService.ReplaceColorTable(texture, table); + _directXService.ReplaceColorTable(texture, table); } _valueIndex = MaterialValueIndex.Invalid; diff --git a/Glamourer/Interop/Material/MaterialService.cs b/Glamourer/Interop/Material/MaterialService.cs index 6b49d3d..48b5fe7 100644 --- a/Glamourer/Interop/Material/MaterialService.cs +++ b/Glamourer/Interop/Material/MaterialService.cs @@ -13,34 +13,6 @@ public static unsafe class MaterialService 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 ReplaceColorTable(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; - } - public static bool GenerateNewColorTable(in ColorTable colorTable, out Texture* texture) { var textureSize = stackalloc int[2]; diff --git a/Glamourer/Interop/Material/MaterialValueIndex.cs b/Glamourer/Interop/Material/MaterialValueIndex.cs index 1229cd7..f461637 100644 --- a/Glamourer/Interop/Material/MaterialValueIndex.cs +++ b/Glamourer/Interop/Material/MaterialValueIndex.cs @@ -109,31 +109,6 @@ public readonly record struct MaterialValueIndex( return true; } - public unsafe bool TryGetColorTable(Actor actor, out MtrlFile.ColorTable table) - { - if (TryGetTexture(actor, out var texture)) - 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 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 static MaterialValueIndex FromKey(uint key) => new(key); diff --git a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs index cb78c47..5901899 100644 --- a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs +++ b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs @@ -1,4 +1,5 @@ -using Glamourer.State; +using Dalamud.Plugin.Services; +using Glamourer.State; using OtterGui.Services; using Penumbra.Api.Enums; @@ -10,13 +11,15 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService private readonly PenumbraService _penumbra; private readonly StateManager _state; private readonly ObjectManager _objects; + private readonly IFramework _framework; - public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects) + public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects, IFramework framework) { _penumbra = penumbra; _config = config; _state = state; _objects = objects; + _framework = framework; _penumbra.ModSettingChanged += OnModSettingChange; } @@ -26,28 +29,31 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService private void OnModSettingChange(ModSettingChange type, string name, string mod, bool inherited) { if (type is ModSettingChange.TemporaryMod) - { - _objects.Update(); - foreach (var (id, state) in _state) + _framework.RunOnFrameworkThread(() => { - if (!_objects.TryGetValue(id, out var actors) || !actors.Valid) - continue; + _objects.Update(); + foreach (var (id, state) in _state) + { + if (!_objects.TryGetValue(id, out var actors) || !actors.Valid) + continue; - var collection = _penumbra.GetActorCollection(actors.Objects[0]); - if (collection != name) - continue; + var collection = _penumbra.GetActorCollection(actors.Objects[0]); + if (collection != name) + continue; - foreach (var actor in actors.Objects) - _state.ReapplyState(actor, state, StateSource.IpcManual); - Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {id.Incognito(null)}."); - } - } + foreach (var actor in actors.Objects) + _state.ReapplyState(actor, state, StateSource.IpcManual); + Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {id.Incognito(null)}."); + } + }); else if (_config.AutoRedrawEquipOnChanges) - { - var playerName = _penumbra.GetCurrentPlayerCollection(); - if (playerName == name) - _state.ReapplyState(_objects.Player, StateSource.IpcManual); - Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {_objects.PlayerData.Identifier.Incognito(null)} (Local Player)."); - } + _framework.RunOnFrameworkThread(() => + { + var playerName = _penumbra.GetCurrentPlayerCollection(); + if (playerName == name) + _state.ReapplyState(_objects.Player, StateSource.IpcManual); + Glamourer.Log.Debug( + $"Automatically applied mod settings of type {type} to {_objects.PlayerData.Identifier.Incognito(null)} (Local Player)."); + }); } } diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index a025cb5..52996ea 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -25,7 +25,8 @@ public class StateApplier( MetaService _metaService, ObjectManager _objects, CrestService _crests, - Configuration _config) + Configuration _config, + DirectXService _directX) { /// Simply force a redraw regardless of conditions. public void ForceRedraw(ActorData data) @@ -286,7 +287,7 @@ public class StateApplier( if (!index.TryGetTexture(actor, out var texture)) continue; - if (!index.TryGetColorTable(texture, out var table)) + if (!_directX.TryGetColorTable(*texture, out var table)) continue; if (value.HasValue) @@ -296,7 +297,43 @@ public class StateApplier( else continue; - MaterialService.ReplaceColorTable(texture, table); + _directX.ReplaceColorTable(texture, table); + } + } + + public ActorData ChangeMaterialValues(ActorState state, bool apply) + { + var data = GetData(state); + if (apply) + ChangeMaterialValues(data, state.Materials, state.IsLocked); + return data; + } + + public unsafe void ChangeMaterialValues(ActorData data, in StateMaterialManager materials, bool force) + { + if (!force && !_config.UseAdvancedDyes) + return; + + var groupedMaterialValues = materials.Values.Select(p => (MaterialValueIndex.FromKey(p.Key), p.Value)) + .GroupBy(p => (p.Item1.DrawObject, p.Item1.SlotIndex, p.Item1.MaterialIndex)); + + foreach (var group in groupedMaterialValues) + { + var values = group.ToList(); + var mainKey = values[0].Item1; + foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true })) + { + if (!mainKey.TryGetTexture(actor, out var texture)) + continue; + + if (!_directX.TryGetColorTable(*texture, out var table)) + continue; + + foreach (var (key, value) in values) + value.Model.Apply(ref table[key.RowIndex]); + + _directX.ReplaceColorTable(texture, table); + } } } @@ -332,17 +369,17 @@ public class StateApplier( ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors; ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand)); - } - if (state.ModelData.IsHuman) - { - ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible()); - ChangeMetaState(actors, MetaIndex.WeaponState, state.ModelData.IsWeaponVisible()); - ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled()); - ChangeCrests(actors, state.ModelData.CrestVisibility); - ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters, state.IsLocked); - foreach (var material in state.Materials.Values) - ChangeMaterialValue(actors, MaterialValueIndex.FromKey(material.Key), material.Value.Model, state.IsLocked); + if (state.ModelData.IsHuman) + { + ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible()); + ChangeMetaState(actors, MetaIndex.WeaponState, state.ModelData.IsWeaponVisible()); + ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled()); + ChangeCrests(actors, state.ModelData.CrestVisibility); + ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters, state.IsLocked); + // This should never be applied when caused through IPC, then redraw should be true. + ChangeMaterialValues(actors, state.Materials, state.IsLocked); + } } return actors; diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 3d1b27c..915aa2c 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -321,6 +321,8 @@ public class StateEditor( settings.Source, out _, settings.Key); } } + + requiresRedraw |= mergedDesign.Design.Materials.Count > 0 && settings.Source.IsIpc(); } var actors = settings.Source.RequiresChange() diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 2c0b2d2..e4772aa 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -224,7 +224,8 @@ public sealed class StateManager( || !state.ModelData.IsHuman || CustomizeArray.Compare(state.ModelData.Customize, state.BaseData.Customize).RequiresRedraw(); - state.ModelData = state.BaseData; + redraw |= state.Materials.Values.Count > 0 && source.IsIpc(); + state.ModelData = state.BaseData; state.ModelData.SetIsWet(false); foreach (var index in Enum.GetValues()) state.Sources[index] = StateSource.Game; @@ -339,15 +340,13 @@ public sealed class StateManager( if (!GetOrCreate(actor, out var state)) return; - var data = Applier.ApplyAll(state, - !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); - StateChanged.Invoke(StateChanged.Type.Reapply, source, state, data, null); + ReapplyState(actor, state, source); } public void ReapplyState(Actor actor, ActorState state, StateSource source) { var data = Applier.ApplyAll(state, - !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); + !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw() || state.Materials.Values.Count > 0 && source.IsIpc(), false); StateChanged.Invoke(StateChanged.Type.Reapply, source, state, data, null); }