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) {