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