Run auto redraw on framework, add some locks, handle material value application differently for ApplyAll.

This commit is contained in:
Ottermandias 2024-02-22 18:10:45 +01:00
parent e5f62d3ea9
commit d8ce81cdc4
10 changed files with 287 additions and 224 deletions

View file

@ -20,13 +20,14 @@ namespace Glamourer.Gui.Materials;
public sealed unsafe class AdvancedDyePopup(
Configuration config,
StateManager stateManager,
LiveColorTablePreviewer preview) : IService
LiveColorTablePreviewer preview,
DirectXService directX) : IService
{
private MaterialValueIndex? _drawIndex;
private ActorState _state = null!;
private Actor _actor;
private byte _selectedMaterial = byte.MaxValue;
private bool _anyChanged = false;
private bool _anyChanged;
private bool ShouldBeDrawn()
{
@ -94,7 +95,7 @@ public sealed unsafe class AdvancedDyePopup(
for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i)
{
var index = _drawIndex!.Value with { MaterialIndex = i };
var available = index.TryGetTexture(textures, out var texture) && index.TryGetColorTable(texture, out var table);
var available = index.TryGetTexture(textures, out var texture) && directX.TryGetColorTable(*texture, out var table);
if (index == preview.LastValueIndex with { RowIndex = 0 })
table = preview.LastOriginalColorTable;
@ -179,7 +180,7 @@ public sealed unsafe class AdvancedDyePopup(
}
}
public unsafe void Draw(Actor actor, ActorState state)
public void Draw(Actor actor, ActorState state)
{
_actor = actor;
_state = state;
@ -236,20 +237,20 @@ public sealed unsafe class AdvancedDyePopup(
? _state.ModelData.Weapon(slot)
: _state.ModelData.Armor(slot).ToWeapon(0);
var value = new MaterialValueState(internalRow, internalRow, weapon, StateSource.Manual);
stateManager.ChangeMaterialValue(_state!, materialIndex with { RowIndex = (byte)idx }, value, ApplySettings.Manual);
stateManager.ChangeMaterialValue(_state, materialIndex with { RowIndex = (byte)idx }, value, ApplySettings.Manual);
}
ImGui.SameLine(0, spacing);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UndoAlt.ToIconString(), buttonSize, "Reset this table to game state.", !_anyChanged,
true))
for (byte i = 0; i < MtrlFile.ColorTable.NumRows; ++i)
stateManager.ResetMaterialValue(_state, materialIndex with { RowIndex = (byte)i }, ApplySettings.Game);
stateManager.ResetMaterialValue(_state, materialIndex with { RowIndex = i }, ApplySettings.Game);
}
private void DrawRow(ref MtrlFile.ColorTable.Row row, MaterialValueIndex index, in MtrlFile.ColorTable table)
{
using var id = ImRaii.PushId(index.RowIndex);
var changed = _state!.Materials.TryGetValue(index, out var value);
var changed = _state.Materials.TryGetValue(index, out var value);
if (!changed)
{
var internalRow = new ColorRow(row);
@ -314,7 +315,7 @@ public sealed unsafe class AdvancedDyePopup(
stateManager.ResetMaterialValue(_state, index, ApplySettings.Game);
if (applied)
stateManager.ChangeMaterialValue(_state!, index, value, ApplySettings.Manual);
stateManager.ChangeMaterialValue(_state, index, value, ApplySettings.Manual);
}
private LabelStruct _label = new();

View file

@ -0,0 +1,187 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Lumina.Data.Files;
using OtterGui.Services;
using Penumbra.String.Functions;
using SharpGen.Runtime;
using Vortice.Direct3D11;
using Vortice.DXGI;
using static Penumbra.GameData.Files.MtrlFile;
using MapFlags = Vortice.Direct3D11.MapFlags;
using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture;
namespace Glamourer.Interop.Material;
public unsafe class DirectXService(IFramework framework) : IService
{
private readonly object _lock = new();
private readonly ConcurrentDictionary<nint, (DateTime Update, ColorTable Table)> _textures = [];
/// <summary> Generate a color table the way the game does inside the original texture, and release the original. </summary>
/// <param name="original"> The original texture that will be replaced with a new one. </param>
/// <param name="colorTable"> The input color table. </param>
/// <returns> Success or failure. </returns>
public bool ReplaceColorTable(Texture** original, in ColorTable colorTable)
{
if (original == null)
return false;
var textureSize = stackalloc int[2];
textureSize[0] = MaterialService.TextureWidth;
textureSize[1] = MaterialService.TextureHeight;
lock (_lock)
{
using var texture = new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1,
(uint)TexFile.TextureFormat.R16G16B16A16F,
(uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7), false);
if (texture.IsInvalid)
return false;
fixed (ColorTable* ptr = &colorTable)
{
if (!texture.Texture->InitializeContents(ptr))
return false;
}
Glamourer.Log.Verbose($"[{Thread.CurrentThread.ManagedThreadId}] Replaced texture {(ulong)*original:X} with new ColorTable.");
texture.Exchange(ref *(nint*)original);
}
return true;
}
public bool TryGetColorTable(Texture* texture, out ColorTable table)
{
if (_textures.TryGetValue((nint)texture, out var p) && framework.LastUpdateUTC == p.Update)
{
table = p.Table;
return true;
}
lock (_lock)
{
if (!TextureColorTable(texture, out table))
return false;
}
_textures[(nint)texture] = (framework.LastUpdateUTC, table);
return true;
}
/// <summary> Try to turn a color table GPU-loaded texture (R16G16B16A16Float, 4 Width, 16 Height) into an actual color table. </summary>
/// <param name="texture"> A pointer to the internal texture struct containing the GPU handle. </param>
/// <param name="table"> The returned color table. </param>
/// <returns> Whether the table could be fetched. </returns>
private static bool TextureColorTable(Texture* texture, out ColorTable table)
{
if (texture == null)
{
table = default;
return false;
}
try
{
// Create direct x resource and ensure that it is kept alive.
using var tex = new ID3D11Texture2D1((nint)texture->D3D11Texture2D);
tex.AddRef();
table = GetResourceData(tex, CreateStagedClone, GetTextureData);
return true;
}
catch
{
return false;
}
}
/// <summary> Create a staging clone of the existing texture handle for stability reasons. </summary>
private static ID3D11Texture2D1 CreateStagedClone(ID3D11Texture2D1 resource)
{
var desc = resource.Description1 with
{
Usage = ResourceUsage.Staging,
BindFlags = 0,
CPUAccessFlags = CpuAccessFlags.Read,
MiscFlags = 0,
};
var ret = resource.Device.As<ID3D11Device3>().CreateTexture2D1(desc);
Glamourer.Log.Excessive(
$"[{Thread.CurrentThread.ManagedThreadId}] Cloning resource {resource.NativePointer:X} to {ret.NativePointer:X}");
return ret;
}
/// <summary> Turn a mapped texture into a color table. </summary>
private static ColorTable GetTextureData(ID3D11Texture2D1 resource, MappedSubresource map)
{
var desc = resource.Description1;
if (desc.Format is not Format.R16G16B16A16_Float
|| desc.Width != MaterialService.TextureWidth
|| desc.Height != MaterialService.TextureHeight
|| map.DepthPitch != map.RowPitch * desc.Height)
throw new InvalidDataException("The texture was not a valid color table texture.");
return ReadTexture(map.DataPointer, map.DepthPitch, desc.Height, map.RowPitch);
}
/// <summary> Transform the GPU data into the color table. </summary>
/// <param name="data"> The pointer to the raw texture data. </param>
/// <param name="length"> The size of the raw texture data. </param>
/// <param name="height"> The height of the texture. (Needs to be 16).</param>
/// <param name="pitch"> The stride in the texture data. </param>
/// <returns></returns>
private static ColorTable ReadTexture(nint data, int length, int height, int pitch)
{
// Check that the data has sufficient dimension and size.
var expectedSize = sizeof(Half) * MaterialService.TextureWidth * height * 4;
if (length < expectedSize || sizeof(ColorTable) != expectedSize || height != MaterialService.TextureHeight)
return default;
var ret = new ColorTable();
var target = (byte*)&ret;
// If the stride is the same as in the table, just copy.
if (pitch == MaterialService.TextureWidth)
MemoryUtility.MemCpyUnchecked(target, (void*)data, length);
// Otherwise, adapt the stride.
else
for (var y = 0; y < height; ++y)
{
MemoryUtility.MemCpyUnchecked(target + y * MaterialService.TextureWidth * sizeof(Half) * 4, (byte*)data + y * pitch,
MaterialService.TextureWidth * sizeof(Half) * 4);
}
return ret;
}
/// <summary> Get resources of a texture. </summary>
private static TRet GetResourceData<T, TRet>(T res, Func<T, T> cloneResource, Func<T, MappedSubresource, TRet> getData)
where T : ID3D11Resource
{
using var stagingRes = cloneResource(res);
res.Device.ImmediateContext.CopyResource(stagingRes, res);
Glamourer.Log.Excessive(
$"[{Thread.CurrentThread.ManagedThreadId}] Copied resource data {res.NativePointer:X} to {stagingRes.NativePointer:X}");
stagingRes.Device.ImmediateContext.Map(stagingRes, 0, MapMode.Read, MapFlags.None, out var mapInfo).CheckError();
Glamourer.Log.Excessive(
$"[{Thread.CurrentThread.ManagedThreadId}] Mapped resource data for {stagingRes.NativePointer:X} to {mapInfo.DataPointer:X}");
try
{
return getData(stagingRes, mapInfo);
}
finally
{
Glamourer.Log.Excessive($"[{Thread.CurrentThread.ManagedThreadId}] Obtained resource data.");
stagingRes.Device.ImmediateContext.Unmap(stagingRes, 0);
Glamourer.Log.Excessive($"[{Thread.CurrentThread.ManagedThreadId}] Unmapped resource data for {stagingRes.NativePointer:X}");
}
}
private static readonly Result WasStillDrawing = new(0x887A000A);
}

View file

@ -1,116 +0,0 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Penumbra.GameData.Files;
using Penumbra.String.Functions;
using SharpGen.Runtime;
using Vortice.Direct3D11;
using Vortice.DXGI;
using MapFlags = Vortice.Direct3D11.MapFlags;
namespace Glamourer.Interop.Material;
public static unsafe class DirectXTextureHelper
{
/// <summary> Try to turn a color table GPU-loaded texture (R16G16B16A16Float, 4 Width, 16 Height) into an actual color table. </summary>
/// <param name="texture"> A pointer to the internal texture struct containing the GPU handle. </param>
/// <param name="table"> The returned color table. </param>
/// <returns> Whether the table could be fetched. </returns>
public static bool TryGetColorTable(Texture* texture, out MtrlFile.ColorTable table)
{
if (texture == null)
{
table = default;
return false;
}
try
{
// Create direct x resource and ensure that it is kept alive.
using var tex = new ID3D11Texture2D1((nint)texture->D3D11Texture2D);
tex.AddRef();
table = GetResourceData(tex, CreateStagedClone, GetTextureData);
return true;
}
catch
{
return false;
}
}
/// <summary> Create a staging clone of the existing texture handle for stability reasons. </summary>
private static ID3D11Texture2D1 CreateStagedClone(ID3D11Texture2D1 resource)
{
var desc = resource.Description1 with
{
Usage = ResourceUsage.Staging,
BindFlags = 0,
CPUAccessFlags = CpuAccessFlags.Read,
MiscFlags = 0,
};
return resource.Device.As<ID3D11Device3>().CreateTexture2D1(desc);
}
/// <summary> Turn a mapped texture into a color table. </summary>
private static MtrlFile.ColorTable GetTextureData(ID3D11Texture2D1 resource, MappedSubresource map)
{
var desc = resource.Description1;
if (desc.Format is not Format.R16G16B16A16_Float
|| desc.Width != MaterialService.TextureWidth
|| desc.Height != MaterialService.TextureHeight
|| map.DepthPitch != map.RowPitch * desc.Height)
throw new InvalidDataException("The texture was not a valid color table texture.");
return ReadTexture(map.DataPointer, map.DepthPitch, desc.Height, map.RowPitch);
}
/// <summary> Transform the GPU data into the color table. </summary>
/// <param name="data"> The pointer to the raw texture data. </param>
/// <param name="length"> The size of the raw texture data. </param>
/// <param name="height"> The height of the texture. (Needs to be 16).</param>
/// <param name="pitch"> The stride in the texture data. </param>
/// <returns></returns>
private static MtrlFile.ColorTable ReadTexture(nint data, int length, int height, int pitch)
{
// Check that the data has sufficient dimension and size.
var expectedSize = sizeof(Half) * MaterialService.TextureWidth * height * 4;
if (length < expectedSize || sizeof(MtrlFile.ColorTable) != expectedSize || height != MaterialService.TextureHeight)
return default;
var ret = new MtrlFile.ColorTable();
var target = (byte*)&ret;
// If the stride is the same as in the table, just copy.
if (pitch == MaterialService.TextureWidth)
MemoryUtility.MemCpyUnchecked(target, (void*)data, length);
// Otherwise, adapt the stride.
else
for (var y = 0; y < height; ++y)
{
MemoryUtility.MemCpyUnchecked(target + y * MaterialService.TextureWidth * sizeof(Half) * 4, (byte*)data + y * pitch,
MaterialService.TextureWidth * sizeof(Half) * 4);
}
return ret;
}
/// <summary> Get resources of a texture. </summary>
private static TRet GetResourceData<T, TRet>(T res, Func<T, T> cloneResource, Func<T, MappedSubresource, TRet> getData)
where T : ID3D11Resource
{
using var stagingRes = cloneResource(res);
res.Device.ImmediateContext.CopyResource(stagingRes, res);
stagingRes.Device.ImmediateContext.Map(stagingRes, 0, MapMode.Read, MapFlags.None, out var mapInfo).CheckError();
try
{
return getData(stagingRes, mapInfo);
}
finally
{
stagingRes.Device.ImmediateContext.Unmap(stagingRes, 0);
}
}
}

View file

@ -9,8 +9,9 @@ namespace Glamourer.Interop.Material;
public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable
{
private readonly IObjectTable _objects;
private readonly IFramework _framework;
private readonly IObjectTable _objects;
private readonly IFramework _framework;
private readonly DirectXService _directXService;
public MaterialValueIndex LastValueIndex { get; private set; } = MaterialValueIndex.Invalid;
public MtrlFile.ColorTable LastOriginalColorTable { get; private set; }
@ -19,11 +20,11 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable
private ObjectIndex _objectIndex = ObjectIndex.AnyIndex;
private MtrlFile.ColorTable _originalColorTable;
public LiveColorTablePreviewer(IObjectTable objects, IFramework framework)
public LiveColorTablePreviewer(IObjectTable objects, IFramework framework, DirectXService directXService)
{
_objects = objects;
_framework = framework;
_directXService = directXService;
_framework.Update += OnFramework;
}
@ -34,7 +35,7 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable
var actor = (Actor)_objects.GetObjectAddress(_lastObjectIndex.Index);
if (actor.IsCharacter && LastValueIndex.TryGetTexture(actor, out var texture))
MaterialService.ReplaceColorTable(texture, LastOriginalColorTable);
_directXService.ReplaceColorTable(texture, LastOriginalColorTable);
LastValueIndex = MaterialValueIndex.Invalid;
_lastObjectIndex = ObjectIndex.AnyIndex;
@ -78,15 +79,14 @@ public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable
}
else
{
for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i)
{
table[i].Diffuse = diffuse;
table[i].Diffuse = diffuse;
table[i].Emissive = emissive;
}
}
MaterialService.ReplaceColorTable(texture, table);
_directXService.ReplaceColorTable(texture, table);
}
_valueIndex = MaterialValueIndex.Invalid;

View file

@ -13,34 +13,6 @@ public static unsafe class MaterialService
public const int TextureHeight = ColorTable.NumRows;
public const int MaterialsPerModel = 4;
/// <summary> Generate a color table the way the game does inside the original texture, and release the original. </summary>
/// <param name="original"> The original texture that will be replaced with a new one. </param>
/// <param name="colorTable"> The input color table. </param>
/// <returns> Success or failure. </returns>
public static bool ReplaceColorTable(Texture** original, in ColorTable colorTable)
{
if (original == null)
return false;
var textureSize = stackalloc int[2];
textureSize[0] = TextureWidth;
textureSize[1] = TextureHeight;
using var texture = new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, (uint)TexFile.TextureFormat.R16G16B16A16F,
(uint)(TexFile.Attribute.TextureType2D | TexFile.Attribute.Managed | TexFile.Attribute.Immutable), 7), false);
if (texture.IsInvalid)
return false;
fixed (ColorTable* ptr = &colorTable)
{
if (!texture.Texture->InitializeContents(ptr))
return false;
}
texture.Exchange(ref *(nint*)original);
return true;
}
public static bool GenerateNewColorTable(in ColorTable colorTable, out Texture* texture)
{
var textureSize = stackalloc int[2];

View file

@ -109,31 +109,6 @@ public readonly record struct MaterialValueIndex(
return true;
}
public unsafe bool TryGetColorTable(Actor actor, out MtrlFile.ColorTable table)
{
if (TryGetTexture(actor, out var texture))
return TryGetColorTable(texture, out table);
table = default;
return false;
}
public unsafe bool TryGetColorTable(Texture** texture, out MtrlFile.ColorTable table)
=> DirectXTextureHelper.TryGetColorTable(*texture, out table);
public bool TryGetColorRow(Actor actor, out MtrlFile.ColorTable.Row row)
{
if (!TryGetColorTable(actor, out var table))
{
row = default;
return false;
}
row = table[RowIndex];
return true;
}
public static MaterialValueIndex FromKey(uint key)
=> new(key);

View file

@ -1,4 +1,5 @@
using Glamourer.State;
using Dalamud.Plugin.Services;
using Glamourer.State;
using OtterGui.Services;
using Penumbra.Api.Enums;
@ -10,13 +11,15 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService
private readonly PenumbraService _penumbra;
private readonly StateManager _state;
private readonly ObjectManager _objects;
private readonly IFramework _framework;
public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects)
public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects, IFramework framework)
{
_penumbra = penumbra;
_config = config;
_state = state;
_objects = objects;
_framework = framework;
_penumbra.ModSettingChanged += OnModSettingChange;
}
@ -26,28 +29,31 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService
private void OnModSettingChange(ModSettingChange type, string name, string mod, bool inherited)
{
if (type is ModSettingChange.TemporaryMod)
{
_objects.Update();
foreach (var (id, state) in _state)
_framework.RunOnFrameworkThread(() =>
{
if (!_objects.TryGetValue(id, out var actors) || !actors.Valid)
continue;
_objects.Update();
foreach (var (id, state) in _state)
{
if (!_objects.TryGetValue(id, out var actors) || !actors.Valid)
continue;
var collection = _penumbra.GetActorCollection(actors.Objects[0]);
if (collection != name)
continue;
var collection = _penumbra.GetActorCollection(actors.Objects[0]);
if (collection != name)
continue;
foreach (var actor in actors.Objects)
_state.ReapplyState(actor, state, StateSource.IpcManual);
Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {id.Incognito(null)}.");
}
}
foreach (var actor in actors.Objects)
_state.ReapplyState(actor, state, StateSource.IpcManual);
Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {id.Incognito(null)}.");
}
});
else if (_config.AutoRedrawEquipOnChanges)
{
var playerName = _penumbra.GetCurrentPlayerCollection();
if (playerName == name)
_state.ReapplyState(_objects.Player, StateSource.IpcManual);
Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {_objects.PlayerData.Identifier.Incognito(null)} (Local Player).");
}
_framework.RunOnFrameworkThread(() =>
{
var playerName = _penumbra.GetCurrentPlayerCollection();
if (playerName == name)
_state.ReapplyState(_objects.Player, StateSource.IpcManual);
Glamourer.Log.Debug(
$"Automatically applied mod settings of type {type} to {_objects.PlayerData.Identifier.Incognito(null)} (Local Player).");
});
}
}

View file

@ -25,7 +25,8 @@ public class StateApplier(
MetaService _metaService,
ObjectManager _objects,
CrestService _crests,
Configuration _config)
Configuration _config,
DirectXService _directX)
{
/// <summary> Simply force a redraw regardless of conditions. </summary>
public void ForceRedraw(ActorData data)
@ -286,7 +287,7 @@ public class StateApplier(
if (!index.TryGetTexture(actor, out var texture))
continue;
if (!index.TryGetColorTable(texture, out var table))
if (!_directX.TryGetColorTable(*texture, out var table))
continue;
if (value.HasValue)
@ -296,7 +297,43 @@ public class StateApplier(
else
continue;
MaterialService.ReplaceColorTable(texture, table);
_directX.ReplaceColorTable(texture, table);
}
}
public ActorData ChangeMaterialValues(ActorState state, bool apply)
{
var data = GetData(state);
if (apply)
ChangeMaterialValues(data, state.Materials, state.IsLocked);
return data;
}
public unsafe void ChangeMaterialValues(ActorData data, in StateMaterialManager materials, bool force)
{
if (!force && !_config.UseAdvancedDyes)
return;
var groupedMaterialValues = materials.Values.Select(p => (MaterialValueIndex.FromKey(p.Key), p.Value))
.GroupBy(p => (p.Item1.DrawObject, p.Item1.SlotIndex, p.Item1.MaterialIndex));
foreach (var group in groupedMaterialValues)
{
var values = group.ToList();
var mainKey = values[0].Item1;
foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true }))
{
if (!mainKey.TryGetTexture(actor, out var texture))
continue;
if (!_directX.TryGetColorTable(*texture, out var table))
continue;
foreach (var (key, value) in values)
value.Model.Apply(ref table[key.RowIndex]);
_directX.ReplaceColorTable(texture, table);
}
}
}
@ -332,17 +369,17 @@ public class StateApplier(
ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand));
var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors;
ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand));
}
if (state.ModelData.IsHuman)
{
ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible());
ChangeMetaState(actors, MetaIndex.WeaponState, state.ModelData.IsWeaponVisible());
ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled());
ChangeCrests(actors, state.ModelData.CrestVisibility);
ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters, state.IsLocked);
foreach (var material in state.Materials.Values)
ChangeMaterialValue(actors, MaterialValueIndex.FromKey(material.Key), material.Value.Model, state.IsLocked);
if (state.ModelData.IsHuman)
{
ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible());
ChangeMetaState(actors, MetaIndex.WeaponState, state.ModelData.IsWeaponVisible());
ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled());
ChangeCrests(actors, state.ModelData.CrestVisibility);
ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters, state.IsLocked);
// This should never be applied when caused through IPC, then redraw should be true.
ChangeMaterialValues(actors, state.Materials, state.IsLocked);
}
}
return actors;

View file

@ -321,6 +321,8 @@ public class StateEditor(
settings.Source, out _, settings.Key);
}
}
requiresRedraw |= mergedDesign.Design.Materials.Count > 0 && settings.Source.IsIpc();
}
var actors = settings.Source.RequiresChange()

View file

@ -224,7 +224,8 @@ public sealed class StateManager(
|| !state.ModelData.IsHuman
|| CustomizeArray.Compare(state.ModelData.Customize, state.BaseData.Customize).RequiresRedraw();
state.ModelData = state.BaseData;
redraw |= state.Materials.Values.Count > 0 && source.IsIpc();
state.ModelData = state.BaseData;
state.ModelData.SetIsWet(false);
foreach (var index in Enum.GetValues<CustomizeIndex>())
state.Sources[index] = StateSource.Game;
@ -339,15 +340,13 @@ public sealed class StateManager(
if (!GetOrCreate(actor, out var state))
return;
var data = Applier.ApplyAll(state,
!actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false);
StateChanged.Invoke(StateChanged.Type.Reapply, source, state, data, null);
ReapplyState(actor, state, source);
}
public void ReapplyState(Actor actor, ActorState state, StateSource source)
{
var data = Applier.ApplyAll(state,
!actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false);
!actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw() || state.Materials.Values.Count > 0 && source.IsIpc(), false);
StateChanged.Invoke(StateChanged.Type.Reapply, source, state, data, null);
}