Add state editing and tracking.

This commit is contained in:
Ottermandias 2024-02-01 13:52:20 +01:00
parent 5cdcb9288e
commit fb7aac5228
10 changed files with 383 additions and 235 deletions

View file

@ -42,6 +42,9 @@ namespace Glamourer.Events
/// <summary> A characters saved state had its customize parameter changed. Data is the old value, the new value and the type [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. </summary>
Parameter,
/// <summary> 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)]. </summary>
MaterialValue,
/// <summary> A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] </summary>
Design,

View file

@ -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<MaterialValueIndex.DrawObjectType> 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);
}
}

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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<Pointer<Texture>> 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<Texture>* 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))

View file

@ -104,7 +104,7 @@ public readonly struct MaterialValueManager<T>
public int RemoveValues(MaterialValueIndex min, MaterialValueIndex max)
{
var (minIdx, maxIdx) = GetMinMax(CollectionsMarshal.AsSpan(_values), min.Key, max.Key);
var (minIdx, maxIdx) = MaterialValueManager.GetMinMax<T>(CollectionsMarshal.AsSpan(_values), min.Key, max.Key);
if (minIdx < 0)
return 0;
@ -114,20 +114,49 @@ public readonly struct MaterialValueManager<T>
}
public ReadOnlySpan<(uint key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max)
=> Filter(CollectionsMarshal.AsSpan(_values), min, max);
=> MaterialValueManager.Filter<T>(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<T>.Instance);
}
public static class MaterialValueManager
{
internal class Comparer<T> : IComparer<(uint Key, T Value)>
{
public static readonly Comparer<T> 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<T>(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex index, out T ret)
{
var idx = values.BinarySearch((index.Key, default!), Comparer<T>.Instance);
if (idx < 0)
{
ret = default!;
return false;
}
ret = values[idx].Value;
return true;
}
public static ReadOnlySpan<(uint Key, T Value)> Filter<T>(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)];
}
/// <summary> Obtain the minimum index and maximum index for a minimum and maximum key. </summary>
private static (int MinIdx, int MaxIdx) GetMinMax(ReadOnlySpan<(uint Key, T Value)> values, uint minKey, uint maxKey)
internal static (int MinIdx, int MaxIdx) GetMinMax<T>(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<T>.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<T>
// 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<T>.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<T>
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);
}
}

View file

@ -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)
/// <summary> Assumes the actor is valid. </summary>
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;
}
}
}
}

View file

@ -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;
}
/// <summary> Change the value of a single material color table entry. </summary>
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)
{

View file

@ -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;
}
/// <summary> Apply the entire state of an actor to all relevant actors, either via immediate redraw or piecewise. </summary>
/// <param name="state"> The state to apply. </param>
/// <param name="redraw"> Whether a redraw should be forced. </param>

View file

@ -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(
/// <inheritdoc/>
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));
}
/// <inheritdoc/>
public void ChangeMetaState(object data, MetaIndex index, bool value, ApplySettings settings)
{