mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
Add state editing and tracking.
This commit is contained in:
parent
5cdcb9288e
commit
fb7aac5228
10 changed files with 383 additions and 235 deletions
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
148
Glamourer/Interop/Material/MaterialManager.cs
Normal file
148
Glamourer/Interop/Material/MaterialManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue