This commit is contained in:
Ottermandias 2023-06-21 18:09:50 +02:00
parent 5e6f797af4
commit 803fd1b247
16 changed files with 521 additions and 132 deletions

View file

@ -72,4 +72,22 @@ public static class EquipFlagExtensions
EquipSlot.LFinger => EquipFlag.LFingerStain, EquipSlot.LFinger => EquipFlag.LFingerStain,
_ => 0, _ => 0,
}; };
public static EquipFlag ToBothFlags(this EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain,
EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain,
EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain,
EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain,
EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain,
EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain,
EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain,
EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain,
EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain,
EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain,
EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain,
EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain,
_ => 0,
};
} }

View file

@ -20,6 +20,7 @@ public class Configuration : IPluginConfiguration, ISavable
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
public bool UseRestrictedGearProtection { get; set; } = true; public bool UseRestrictedGearProtection { get; set; } = true;
public bool OpenFoldersByDefault { get; set; } = false; public bool OpenFoldersByDefault { get; set; } = false;
public bool AutoRedrawEquipOnChanges { get; set; } = false;
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);

View file

@ -37,7 +37,7 @@ public class Design : ISavable
/// <summary> Unconditionally apply a design to a designdata. </summary> /// <summary> Unconditionally apply a design to a designdata. </summary>
/// <returns>Whether a redraw is required for the changes to take effect.</returns> /// <returns>Whether a redraw is required for the changes to take effect.</returns>
public bool ApplyDesign(ref DesignData data) public (bool, CustomizeFlag, EquipFlag) ApplyDesign(ref DesignData data)
{ {
var modelChanged = data.ModelId != DesignData.ModelId; var modelChanged = data.ModelId != DesignData.ModelId;
data.ModelId = DesignData.ModelId; data.ModelId = DesignData.ModelId;
@ -52,13 +52,16 @@ public class Design : ISavable
customizeFlags |= index.ToFlag(); customizeFlags |= index.ToFlag();
} }
EquipFlag equipFlags = 0;
foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)) foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand))
{ {
if (DoApplyEquip(slot)) if (DoApplyEquip(slot))
data.SetItem(slot, DesignData.Item(slot)); if (data.SetItem(slot, DesignData.Item(slot)))
equipFlags |= slot.ToFlag();
if (DoApplyStain(slot)) if (DoApplyStain(slot))
data.SetStain(slot, DesignData.Stain(slot)); if (data.SetStain(slot, DesignData.Stain(slot)))
equipFlags |= slot.ToStainFlag();
} }
if (DoApplyHatVisible()) if (DoApplyHatVisible())
@ -72,7 +75,7 @@ public class Design : ISavable
if (DoApplyWetness()) if (DoApplyWetness())
data.SetIsWet(DesignData.IsWet()); data.SetIsWet(DesignData.IsWet());
return modelChanged || customizeFlags.RequiresRedraw(); return (modelChanged, customizeFlags, equipFlags);
} }
#endregion #endregion

View file

@ -63,6 +63,24 @@ public unsafe struct DesignData
// @formatter:on // @formatter:on
}; };
public readonly CharacterArmor Armor(EquipSlot slot)
{
fixed (byte* ptr = _equipmentBytes)
{
var armorPtr = (CharacterArmor*)ptr;
return armorPtr[slot.ToIndex()];
}
}
public readonly CharacterWeapon Weapon(EquipSlot slot)
{
fixed (byte* ptr = _equipmentBytes)
{
var armorPtr = (CharacterArmor*)ptr;
return armorPtr[slot is EquipSlot.MainHand ? 10 : 11].ToWeapon(_secondaryMainhand);
}
}
public bool SetItem(EquipSlot slot, EquipItem item) public bool SetItem(EquipSlot slot, EquipItem item)
{ {
var index = slot.ToIndex(); var index = slot.ToIndex();

View file

@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface; using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop; using Glamourer.Interop;
using Glamourer.Interop.Penumbra; using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs; using Glamourer.Interop.Structs;
@ -854,11 +856,81 @@ public unsafe class DebugTab : ITab
} }
} }
public void DrawState(ActorData data, ActorState state)
{
using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
ImGuiUtil.DrawTableColumn("Name");
ImGuiUtil.DrawTableColumn(state.Identifier.ToString());
ImGui.TableNextColumn();
if (ImGui.Button("Reset"))
_state.ResetState(state);
ImGui.TableNextRow();
static void PrintRow<T>(string label, T actor, T model, StateChanged.Source source) where T : notnull
{
ImGuiUtil.DrawTableColumn(label);
ImGuiUtil.DrawTableColumn(actor.ToString()!);
ImGuiUtil.DrawTableColumn(model.ToString()!);
ImGuiUtil.DrawTableColumn(source.ToString());
}
static string ItemString(in DesignData data, EquipSlot slot)
{
var item = data.Item(slot);
return $"{item.Name} ({item.ModelId.Value}{(item.WeaponType != 0 ? $"-{item.WeaponType.Value}" : string.Empty)}-{item.Variant})";
}
PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaFlag.ModelId]);
ImGui.TableNextRow();
PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaFlag.Wetness]);
ImGui.TableNextRow();
if (state.BaseData.ModelId == 0 && state.ModelData.ModelId == 0)
{
PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaFlag.HatState]);
ImGui.TableNextRow();
PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(),
state[ActorState.MetaFlag.VisorState]);
ImGui.TableNextRow();
PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(),
state[ActorState.MetaFlag.WeaponState]);
ImGui.TableNextRow();
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state[slot, false]);
ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).Value.ToString());
ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).Value.ToString());
ImGuiUtil.DrawTableColumn(state[slot, true].ToString());
}
foreach (var type in Enum.GetValues<CustomizeIndex>())
{
PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, state[type]);
ImGui.TableNextRow();
}
}
else
{
ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetCustomizeBytes().Select(b => b.ToString("X2"))));
ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetCustomizeBytes().Select(b => b.ToString("X2"))));
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetEquipmentBytes().Select(b => b.ToString("X2"))));
ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetEquipmentBytes().Select(b => b.ToString("X2"))));
}
}
public static void DrawDesignData(in DesignData data) public static void DrawDesignData(in DesignData data)
{ {
if (data.ModelId == 0) if (data.ModelId == 0)
{ {
using var table = ImRaii.Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); using var table = ImRaii.Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{ {
var item = data.Item(slot); var item = data.Item(slot);
@ -1011,7 +1083,7 @@ public unsafe class DebugTab : ITab
continue; continue;
if (_state.GetOrCreate(identifier, actors.Objects[0], out var state)) if (_state.GetOrCreate(identifier, actors.Objects[0], out var state))
DrawDesignData(state.ModelData); DrawState(actors, state);
else else
ImGui.TextUnformatted("Invalid actor."); ImGui.TextUnformatted("Invalid actor.");
} }
@ -1026,8 +1098,10 @@ public unsafe class DebugTab : ITab
foreach (var (identifier, state) in _state.Where(kvp => !_objectManager.ContainsKey(kvp.Key))) foreach (var (identifier, state) in _state.Where(kvp => !_objectManager.ContainsKey(kvp.Key)))
{ {
using var t = ImRaii.TreeNode(identifier.ToString()); using var t = ImRaii.TreeNode(identifier.ToString());
if (t) if (!t)
DrawDesignData(state.ModelData); return;
DrawState(ActorData.Invalid, state);
} }
} }

View file

@ -1,21 +1,44 @@
using System.Numerics; using System.Numerics;
using Glamourer.Customization;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Gui.Customization; using Glamourer.Gui.Customization;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.DesignTab; namespace Glamourer.Gui.Tabs.DesignTab;
public class DesignPanel public class DesignPanel
{ {
private readonly ObjectManager _objects;
private readonly DesignFileSystemSelector _selector; private readonly DesignFileSystemSelector _selector;
private readonly DesignManager _manager; private readonly DesignManager _manager;
private readonly CustomizationDrawer _customizationDrawer; private readonly CustomizationDrawer _customizationDrawer;
private readonly StateManager _state;
private readonly PenumbraService _penumbra;
private readonly UpdateSlotService _updateSlot;
private readonly WeaponService _weaponService;
private readonly ChangeCustomizeService _changeCustomizeService;
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager) public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects,
StateManager state, PenumbraService penumbra, ChangeCustomizeService changeCustomizeService, WeaponService weaponService,
UpdateSlotService updateSlot)
{ {
_selector = selector; _selector = selector;
_customizationDrawer = customizationDrawer; _customizationDrawer = customizationDrawer;
_manager = manager; _manager = manager;
_objects = objects;
_state = state;
_penumbra = penumbra;
_changeCustomizeService = changeCustomizeService;
_weaponService = weaponService;
_updateSlot = updateSlot;
} }
public void Draw() public void Draw()
@ -28,6 +51,14 @@ public class DesignPanel
if (!child) if (!child)
return; return;
if (ImGui.Button("TEST"))
{
var (id, data) = _objects.PlayerData;
if (data.Valid && _state.GetOrCreate(id, data.Objects[0], out var state))
_state.ApplyDesign(design, state);
}
_customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected()); _customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected());
} }
} }

View file

@ -2,6 +2,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Dalamud.Interface; using Dalamud.Interface;
using Glamourer.Gui.Tabs.DesignTab; using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Interop.Penumbra;
using Glamourer.State; using Glamourer.State;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
@ -15,12 +16,14 @@ public class SettingsTab : ITab
private readonly Configuration _config; private readonly Configuration _config;
private readonly DesignFileSystemSelector _selector; private readonly DesignFileSystemSelector _selector;
private readonly StateListener _stateListener; private readonly StateListener _stateListener;
private readonly PenumbraAutoRedraw _autoRedraw;
public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener) public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, PenumbraAutoRedraw autoRedraw)
{ {
_config = config; _config = config;
_selector = selector; _selector = selector;
_stateListener = stateListener; _stateListener = stateListener;
_autoRedraw = autoRedraw;
} }
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
@ -31,10 +34,14 @@ public class SettingsTab : ITab
using var child = ImRaii.Child("MainWindowChild"); using var child = ImRaii.Child("MainWindowChild");
if (!child) if (!child)
return; return;
Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable); Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable);
Checkbox("Restricted Gear Protection", Checkbox("Restricted Gear Protection",
"Use gender- and race-appropriate models when detecting certain items not available for a characters current gender and race.", "Use gender- and race-appropriate models when detecting certain items not available for a characters current gender and race.",
_config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v); _config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v);
Checkbox("Auto-Reload Gear",
"Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection.",
_config.AutoRedrawEquipOnChanges, _autoRedraw.SetState);
if (Widget.DoubleModifierSelector("Design Deletion Modifier", if (Widget.DoubleModifierSelector("Design Deletion Modifier",
"A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, "A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale,
_config.DeleteDesignModifier, v => _config.DeleteDesignModifier = v)) _config.DeleteDesignModifier, v => _config.DeleteDesignModifier = v))

View file

@ -0,0 +1,67 @@
using System;
using Glamourer.State;
using Penumbra.Api.Enums;
namespace Glamourer.Interop.Penumbra;
public class PenumbraAutoRedraw : IDisposable
{
private readonly Configuration _config;
private readonly PenumbraService _penumbra;
private readonly StateManager _state;
private readonly ObjectManager _objects;
private bool _enabled;
public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects)
{
_penumbra = penumbra;
_config = config;
_state = state;
_objects = objects;
if (_config.AutoRedrawEquipOnChanges)
Enable();
}
public void SetState(bool value)
{
if (value == _config.AutoRedrawEquipOnChanges)
return;
_config.AutoRedrawEquipOnChanges = value;
_config.Save();
if (value)
Enable();
else
Disable();
}
public void Enable()
{
if (_enabled)
return;
_penumbra.ModSettingChanged += OnModSettingChange;
_enabled = true;
}
public void Disable()
{
if (!_enabled)
return;
_penumbra.ModSettingChanged -= OnModSettingChange;
_enabled = false;
}
public void Dispose()
{
Disable();
}
private void OnModSettingChange(ModSettingChange type, string name, string mod, bool inherited)
{
var playerName = _penumbra.GetCurrentPlayerCollection();
if (playerName == name)
_state.ReapplyState(_objects.Player);
}
}

View file

@ -18,9 +18,11 @@ public unsafe class PenumbraService : IDisposable
private readonly EventSubscriber<MouseButton, ChangedItemType, uint> _clickSubscriber; private readonly EventSubscriber<MouseButton, ChangedItemType, uint> _clickSubscriber;
private readonly EventSubscriber<nint, string, nint, nint, nint> _creatingCharacterBase; private readonly EventSubscriber<nint, string, nint, nint, nint> _creatingCharacterBase;
private readonly EventSubscriber<nint, string, nint> _createdCharacterBase; private readonly EventSubscriber<nint, string, nint> _createdCharacterBase;
private readonly EventSubscriber<ModSettingChange, string, string, bool> _modSettingChanged;
private ActionSubscriber<int, RedrawType> _redrawSubscriber; private ActionSubscriber<int, RedrawType> _redrawSubscriber;
private FuncSubscriber<nint, (nint, string)> _drawObjectInfo; private FuncSubscriber<nint, (nint, string)> _drawObjectInfo;
private FuncSubscriber<int, int> _cutsceneParent; private FuncSubscriber<int, int> _cutsceneParent;
private FuncSubscriber<int, (bool, bool, string)> _objectCollection;
private readonly EventSubscriber _initializedEvent; private readonly EventSubscriber _initializedEvent;
private readonly EventSubscriber _disposedEvent; private readonly EventSubscriber _disposedEvent;
@ -35,6 +37,7 @@ public unsafe class PenumbraService : IDisposable
_clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi); _clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi);
_createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi); _createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi);
_creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi); _creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi);
_modSettingChanged = Ipc.ModSettingChanged.Subscriber(pi);
Reattach(); Reattach();
} }
@ -63,6 +66,22 @@ public unsafe class PenumbraService : IDisposable
remove => _createdCharacterBase.Event -= value; remove => _createdCharacterBase.Event -= value;
} }
public event Action<ModSettingChange, string, string, bool> ModSettingChanged
{
add => _modSettingChanged.Event += value;
remove => _modSettingChanged.Event -= value;
}
/// <summary> Obtain the name of the collection currently assigned to the player. </summary>
public string GetCurrentPlayerCollection()
{
if (!Available)
return string.Empty;
var (valid, _, name) = _objectCollection.Invoke(0);
return valid ? name : string.Empty;
}
/// <summary> Obtain the game object corresponding to a draw object. </summary> /// <summary> Obtain the game object corresponding to a draw object. </summary>
public Actor GameObjectFromDrawObject(Model drawObject) public Actor GameObjectFromDrawObject(Model drawObject)
=> Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null; => Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null;
@ -103,9 +122,11 @@ public unsafe class PenumbraService : IDisposable
_clickSubscriber.Enable(); _clickSubscriber.Enable();
_creatingCharacterBase.Enable(); _creatingCharacterBase.Enable();
_createdCharacterBase.Enable(); _createdCharacterBase.Enable();
_modSettingChanged.Enable();
_drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface); _drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface);
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface); _cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface); _redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
_objectCollection = Ipc.GetCollectionForObject.Subscriber(_pluginInterface);
Available = true; Available = true;
Glamourer.Log.Debug("Glamourer attached to Penumbra."); Glamourer.Log.Debug("Glamourer attached to Penumbra.");
} }
@ -122,6 +143,7 @@ public unsafe class PenumbraService : IDisposable
_clickSubscriber.Disable(); _clickSubscriber.Disable();
_creatingCharacterBase.Disable(); _creatingCharacterBase.Disable();
_createdCharacterBase.Disable(); _createdCharacterBase.Disable();
_modSettingChanged.Disable();
if (Available) if (Available)
{ {
Available = false; Available = false;
@ -138,5 +160,6 @@ public unsafe class PenumbraService : IDisposable
_createdCharacterBase.Dispose(); _createdCharacterBase.Dispose();
_initializedEvent.Dispose(); _initializedEvent.Dispose();
_disposedEvent.Dispose(); _disposedEvent.Dispose();
_modSettingChanged.Dispose();
} }
} }

View file

@ -1,4 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using OtterGui.Log;
namespace Glamourer.Interop.Structs; namespace Glamourer.Interop.Structs;
@ -26,4 +28,12 @@ public readonly struct ActorData
Objects = new List<Actor>(0); Objects = new List<Actor>(0);
Label = string.Empty; Label = string.Empty;
} }
public LazyString ToLazyString(string invalid)
{
var objects = Objects;
return Valid
? new LazyString(() => string.Join(", ", objects.Select(o => o.ToString())))
: new LazyString(() => invalid);
}
} }

View file

@ -5,6 +5,7 @@ using Glamourer.Events;
using Glamourer.Interop.Structs; using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Glamourer.Interop; namespace Glamourer.Interop;
@ -31,24 +32,18 @@ public unsafe class UpdateSlotService : IDisposable
{ {
if (!drawObject.IsCharacterBase) if (!drawObject.IsCharacterBase)
return; return;
FlagSlotForUpdateInterop(drawObject, slot, data); FlagSlotForUpdateInterop(drawObject, slot, data);
} }
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor data) public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain)
{ => UpdateSlot(drawObject, slot, armor.With(stain));
if (!drawObject.IsCharacterBase)
return;
FlagSlotForUpdateInterop(drawObject, slot, data.With(drawObject.GetArmor(slot).Stain)); public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor)
} => UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain);
public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain) public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain)
{ => UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain);
if (!drawObject.IsHuman)
return;
FlagSlotForUpdateInterop(drawObject, slot, drawObject.GetArmor(slot).With(stain));
}
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{ {

View file

@ -69,7 +69,8 @@ public static class ServiceManager
.AddSingleton<UpdateSlotService>() .AddSingleton<UpdateSlotService>()
.AddSingleton<WeaponService>() .AddSingleton<WeaponService>()
.AddSingleton<PenumbraService>() .AddSingleton<PenumbraService>()
.AddSingleton<ObjectManager>(); .AddSingleton<ObjectManager>()
.AddSingleton<PenumbraAutoRedraw>();
private static IServiceCollection AddDesigns(this IServiceCollection services) private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>() => services.AddSingleton<DesignManager>()
@ -77,6 +78,7 @@ public static class ServiceManager
private static IServiceCollection AddState(this IServiceCollection services) private static IServiceCollection AddState(this IServiceCollection services)
=> services.AddSingleton<StateManager>() => services.AddSingleton<StateManager>()
.AddSingleton<StateEditor>()
.AddSingleton<StateListener>(); .AddSingleton<StateListener>();
private static IServiceCollection AddUi(this IServiceCollection services) private static IServiceCollection AddUi(this IServiceCollection services)

View file

@ -17,14 +17,20 @@ public class ActorState
HatState, HatState,
VisorState, VisorState,
WeaponState, WeaponState,
ModelId,
} }
public ActorIdentifier Identifier { get; internal init; } public ActorIdentifier Identifier { get; internal init; }
public DesignData ActorData;
/// <summary> This should always represent the unmodified state of the draw object. </summary>
public DesignData BaseData;
/// <summary> This should be the desired state of the draw object. </summary>
public DesignData ModelData; public DesignData ModelData;
/// <summary> This contains whether a change to the base data was made by the game, the user via manual input or through automatic application. </summary>
private readonly StateChanged.Source[] _sources = Enumerable private readonly StateChanged.Source[] _sources = Enumerable
.Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 4).ToArray(); .Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray();
internal ActorState(ActorIdentifier identifier) internal ActorState(ActorIdentifier identifier)
=> Identifier = identifier; => Identifier = identifier;

View file

@ -26,6 +26,7 @@ public class StateEditor
_items = items; _items = items;
} }
public void ChangeCustomize(ActorData data, Customize customize) public void ChangeCustomize(ActorData data, Customize customize)
{ {
foreach (var actor in data.Objects) foreach (var actor in data.Objects)
@ -43,19 +44,15 @@ public class StateEditor
} }
} }
public void ChangeArmor(ActorData data, EquipSlot slot, EquipItem item) public void ChangeArmor(ActorState state, ActorData data, EquipSlot slot)
{ {
var idx = slot.ToIndex(); var armor = state.ModelData.Armor(slot);
if (idx >= 10)
return;
var armor = item.Armor();
foreach (var actor in data.Objects.Where(a => a.IsCharacter)) foreach (var actor in data.Objects.Where(a => a.IsCharacter))
{ {
var mdl = actor.Model; var mdl = actor.Model;
var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize(); var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize();
var (_, resolvedItem) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); var (_, resolvedItem) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
_updateSlot.UpdateArmor(actor.Model, slot, resolvedItem); _updateSlot.UpdateSlot(actor.Model, slot, resolvedItem);
} }
} }

View file

@ -61,42 +61,46 @@ public class StateListener : IDisposable
Unsubscribe(); Unsubscribe();
} }
private enum UpdateState
{
NoChange,
Transformed,
Change,
}
private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr) private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr)
{ {
// TODO: Fixed Designs. // TODO: Fixed Designs.
var actor = (Actor)actorPtr; var actor = (Actor)actorPtr;
var identifier = actor.GetIdentifier(_actors.AwaitedService); var identifier = actor.GetIdentifier(_actors.AwaitedService);
if (*(int*)modelPtr != actor.AsCharacter->ModelCharaId) var modelId = *(uint*)modelPtr;
return;
ref var customize = ref *(Customize*)customizePtr; ref var customize = ref *(Customize*)customizePtr;
if (_manager.TryGetValue(identifier, out var state)) if (_manager.TryGetValue(identifier, out var state))
switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr))
{ {
ApplyCustomize(actor, state, ref customize); case UpdateState.Change: break;
ApplyEquipment(actor, state, (CharacterArmor*)equipDataPtr); case UpdateState.Transformed: break;
if (_config.UseRestrictedGearProtection) case UpdateState.NoChange:
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender); UpdateBaseData(actor, state, customize);
break;
} }
else if (_config.UseRestrictedGearProtection && *(uint*)modelPtr == 0)
{ if (_config.UseRestrictedGearProtection && modelId == 0)
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender); ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
} }
}
private void OnSlotUpdating(Model model, EquipSlot slot, Ref<CharacterArmor> armor, Ref<ulong> returnValue) private void OnSlotUpdating(Model model, EquipSlot slot, Ref<CharacterArmor> armor, Ref<ulong> returnValue)
{ {
// TODO handle hat state // TODO handle hat state
// TODO handle fixed designs
var actor = _penumbra.GameObjectFromDrawObject(model); var actor = _penumbra.GameObjectFromDrawObject(model);
var customize = model.GetCustomize(); var customize = model.GetCustomize();
if (actor.Identifier(_actors.AwaitedService, out var identifier) if (actor.Identifier(_actors.AwaitedService, out var identifier)
&& _manager.TryGetValue(identifier, out var state)) && _manager.TryGetValue(identifier, out var state))
ApplyEquipmentPiece(actor, state, slot, ref armor.Value); ApplyEquipmentPiece(actor, state, slot, ref armor.Value);
var (replaced, replacedArmor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); if (_config.UseRestrictedGearProtection)
if (replaced) (_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
armor.Assign(replacedArmor);
} }
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon) private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
@ -111,7 +115,7 @@ public class StateListener : IDisposable
|| actorWeapon.Type.Value != stateItem.WeaponType || actorWeapon.Type.Value != stateItem.WeaponType
|| actorWeapon.Variant != stateItem.Variant) || actorWeapon.Variant != stateItem.Variant)
{ {
var oldActorItem = state.ActorData.Item(slot); var oldActorItem = state.BaseData.Item(slot);
if (oldActorItem.ModelId.Value == actorWeapon.Set.Value if (oldActorItem.ModelId.Value == actorWeapon.Set.Value
&& oldActorItem.WeaponType.Value == actorWeapon.Type.Value && oldActorItem.WeaponType.Value == actorWeapon.Type.Value
&& oldActorItem.Variant == actorWeapon.Variant) && oldActorItem.Variant == actorWeapon.Variant)
@ -123,8 +127,8 @@ public class StateListener : IDisposable
else else
{ {
var identified = _items.Identify(slot, actorWeapon.Set, actorWeapon.Type, (byte)actorWeapon.Variant, var identified = _items.Identify(slot, actorWeapon.Set, actorWeapon.Type, (byte)actorWeapon.Variant,
slot == EquipSlot.OffHand ? state.ActorData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown); slot == EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
state.ActorData.SetItem(slot, identified); state.BaseData.SetItem(slot, identified);
if (state[slot, false] is not StateChanged.Source.Fixed) if (state[slot, false] is not StateChanged.Source.Fixed)
{ {
state.ModelData.SetItem(slot, identified); state.ModelData.SetItem(slot, identified);
@ -142,7 +146,7 @@ public class StateListener : IDisposable
var stateStain = state.ModelData.Stain(slot); var stateStain = state.ModelData.Stain(slot);
if (actorWeapon.Stain.Value != stateStain.Value) if (actorWeapon.Stain.Value != stateStain.Value)
{ {
var oldActorStain = state.ActorData.Stain(slot); var oldActorStain = state.BaseData.Stain(slot);
if (state[slot, true] is not StateChanged.Source.Fixed) if (state[slot, true] is not StateChanged.Source.Fixed)
{ {
state.ModelData.SetStain(slot, actorWeapon.Stain); state.ModelData.SetStain(slot, actorWeapon.Stain);
@ -159,7 +163,7 @@ public class StateListener : IDisposable
private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize) private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize)
{ {
var actorCustomize = actor.GetCustomize(); var actorCustomize = actor.GetCustomize();
ref var oldActorCustomize = ref state.ActorData.Customize; ref var oldActorCustomize = ref state.BaseData.Customize;
ref var stateCustomize = ref state.ModelData.Customize; ref var stateCustomize = ref state.ModelData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>()) foreach (var idx in Enum.GetValues<CustomizeIndex>())
{ {
@ -201,57 +205,34 @@ public class StateListener : IDisposable
private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor) private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
{ {
var actorArmor = actor.GetArmor(slot); var changeState = UpdateBaseData(actor, state, slot, armor);
if (armor.Value != actorArmor.Value) if (changeState is UpdateState.Transformed)
return; return;
var stateArmor = state.ModelData.Item(slot); if (changeState is UpdateState.NoChange)
if (armor.Set.Value != stateArmor.ModelId.Value || armor.Variant != stateArmor.Variant)
{ {
var oldActorArmor = state.ActorData.Item(slot); armor = state.ModelData.Armor(slot);
if (oldActorArmor.ModelId.Value == actorArmor.Set.Value && oldActorArmor.Variant == actorArmor.Variant)
{
armor.Set = stateArmor.ModelId;
armor.Variant = stateArmor.Variant;
} }
else else
{ {
var identified = _items.Identify(slot, actorArmor.Set, actorArmor.Variant); var modelArmor = state.ModelData.Armor(slot);
state.ActorData.SetItem(slot, identified); if (armor.Value == modelArmor.Value)
if (state[slot, false] is not StateChanged.Source.Fixed) return;
if (state[slot, false] is StateChanged.Source.Fixed)
{ {
state.ModelData.SetItem(slot, identified); armor.Set = modelArmor.Set;
state[slot, false] = StateChanged.Source.Game; armor.Variant = modelArmor.Variant;
} }
else else
{ {
armor.Set = stateArmor.ModelId; _manager.ChangeEquip(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game);
armor.Variant = stateArmor.Variant;
}
}
} }
var stateStain = state.ModelData.Stain(slot); if (state[slot, true] is StateChanged.Source.Fixed)
if (armor.Stain.Value != stateStain.Value) armor.Stain = modelArmor.Stain;
{
var oldActorStain = state.ActorData.Stain(slot);
if (oldActorStain.Value == actorArmor.Stain.Value)
{
armor.Stain = stateStain;
}
else else
{ _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
state.ActorData.SetStain(slot, actorArmor.Stain);
if (state[slot, true] is not StateChanged.Source.Fixed)
{
state.ModelData.SetStain(slot, actorArmor.Stain);
state[slot, true] = StateChanged.Source.Game;
}
else
{
armor.Stain = stateStain;
}
}
} }
} }
@ -280,4 +261,81 @@ public class StateListener : IDisposable
_slotUpdating.Unsubscribe(OnSlotUpdating); _slotUpdating.Unsubscribe(OnSlotUpdating);
_weaponLoading.Unsubscribe(OnWeaponLoading); _weaponLoading.Unsubscribe(OnWeaponLoading);
} }
private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterArmor armor)
{
var actorArmor = actor.GetArmor(slot);
// The actor armor does not correspond to the model armor, thus the actor is transformed.
if (actorArmor.Value != armor.Value)
return UpdateState.Transformed;
// TODO: Hat State.
var baseData = state.BaseData.Armor(slot);
var change = UpdateState.NoChange;
if (baseData.Stain != armor.Stain)
{
state.BaseData.SetStain(slot, armor.Stain);
change = UpdateState.Change;
}
if (baseData.Set.Value != armor.Set.Value || baseData.Variant != armor.Variant)
{
var item = _items.Identify(slot, armor.Set, armor.Variant);
state.BaseData.SetItem(slot, item);
change = UpdateState.Change;
}
return change;
}
private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon)
{
var baseData = state.BaseData.Weapon(slot);
var change = UpdateState.NoChange;
if (baseData.Stain != weapon.Stain)
{
state.BaseData.SetStain(slot, weapon.Stain);
change = UpdateState.Change;
}
if (baseData.Set.Value != weapon.Set.Value || baseData.Type.Value != weapon.Type.Value || baseData.Variant != weapon.Variant)
{
var item = _items.Identify(slot, weapon.Set, weapon.Type, (byte)weapon.Variant,
slot is EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
state.BaseData.SetItem(slot, item);
change = UpdateState.Change;
}
return change;
}
private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, uint modelId, nint customizeData, nint equipData)
{
if (modelId != (uint)actor.AsCharacter->CharacterData.ModelCharaId)
return UpdateState.Transformed;
if (modelId == state.BaseData.ModelId)
return UpdateState.NoChange;
if (modelId == 0)
state.BaseData.LoadNonHuman(modelId, *(Customize*)customizeData, (byte*)equipData);
else
state.BaseData = _manager.FromActor(actor);
return UpdateState.Change;
}
private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize)
{
if (!actor.GetCustomize().Equals(customize))
return UpdateState.Transformed;
if (state.BaseData.Customize.Equals(customize))
return UpdateState.NoChange;
state.BaseData.Customize.Load(customize);
return UpdateState.Change;
}
} }

View file

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Security.Cryptography;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Events; using Glamourer.Events;
@ -11,6 +12,7 @@ using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs; using Glamourer.Interop.Structs;
using Glamourer.Services; using Glamourer.Services;
using Glamourer.Structs; using Glamourer.Structs;
using OtterGui.Log;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -24,13 +26,15 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
private readonly CustomizationService _customizations; private readonly CustomizationService _customizations;
private readonly VisorService _visor; private readonly VisorService _visor;
private readonly StateChanged _event; private readonly StateChanged _event;
private readonly ObjectManager _objects;
private readonly StateEditor _editor;
private readonly PenumbraService _penumbra; private readonly PenumbraService _penumbra;
private readonly Dictionary<ActorIdentifier, ActorState> _states = new(); private readonly Dictionary<ActorIdentifier, ActorState> _states = new();
public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event, public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event,
PenumbraService penumbra) PenumbraService penumbra, ObjectManager objects, StateEditor editor)
{ {
_actors = actors; _actors = actors;
_items = items; _items = items;
@ -38,7 +42,8 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
_visor = visor; _visor = visor;
_event = @event; _event = @event;
_penumbra = penumbra; _penumbra = penumbra;
_objects = objects;
_editor = editor;
} }
public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state)
@ -55,7 +60,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
state = new ActorState(identifier) state = new ActorState(identifier)
{ {
ModelData = designData, ModelData = designData,
ActorData = designData BaseData = designData,
}; };
_states.Add(identifier, state); _states.Add(identifier, state);
return true; return true;
@ -165,9 +170,9 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
return ret; return ret;
} }
if (actor.AsCharacter->ModelCharaId != 0) if (actor.AsCharacter->CharacterData.ModelCharaId != 0)
{ {
ret.LoadNonHuman((uint)actor.AsCharacter->ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData, ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData,
(byte*)&actor.AsCharacter->DrawData.Head); (byte*)&actor.AsCharacter->DrawData.Head);
return ret; return ret;
} }
@ -242,6 +247,94 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
$"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}."); $"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}.");
_event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx)); _event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx));
} }
public void ApplyDesign(Design design, ActorState state)
{
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
switch (design.DoApplyEquip(slot), design.DoApplyStain(slot))
{
case (false, false): continue;
case (true, false):
ChangeEquip(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual);
break;
case (false, true):
ChangeStain(state, slot, design.DesignData.Stain(slot), StateChanged.Source.Manual);
break;
case (true, true):
ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), StateChanged.Source.Manual);
break;
}
}
}
public void ResetState(ActorState state)
{
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
ChangeEquip(state, slot, state.BaseData.Item(slot), state.BaseData.Stain(slot), StateChanged.Source.Game);
_editor.ChangeArmor(state, objects, slot);
}
}
public void ReapplyState(Actor actor)
{
if (!GetOrCreate(actor, out var state))
return;
_objects.Update();
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
_editor.ChangeArmor(state, objects, slot);
}
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source)
{
var old = state.ModelData.Item(slot);
state.ModelData.SetItem(slot, item);
state[slot, false] = source;
_objects.Update();
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
if (source is StateChanged.Source.Manual)
_editor.ChangeArmor(state, objects, slot);
Glamourer.Log.Verbose(
$"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot));
}
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source)
{
var old = state.ModelData.Item(slot);
var oldStain = state.ModelData.Stain(slot);
state.ModelData.SetItem(slot, item);
state.ModelData.SetStain(slot, stain);
state[slot, false] = source;
state[slot, true] = source;
_objects.Update();
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
if (source is StateChanged.Source.Manual)
_editor.ChangeArmor(state, objects, slot);
Glamourer.Log.Verbose(
$"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}) and its stain from {oldStain.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot));
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (oldStain, stain, slot));
}
public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source)
{
var old = state.ModelData.Stain(slot);
state.ModelData.SetStain(slot, stain);
state[slot, true] = source;
_objects.Update();
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
if (source is StateChanged.Source.Manual)
_editor.ChangeArmor(state, objects, slot);
Glamourer.Log.Verbose(
$"Set {slot.ToName()} stain in state {state.Identifier} from {old.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (old, stain, slot));
}
// //
///// <summary> Change whether to apply a specific customize value. </summary> ///// <summary> Change whether to apply a specific customize value. </summary>
//public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) //public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
@ -255,21 +348,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); // _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx);
//} //}
// //
///// <summary> Change a non-weapon equipment piece. </summary>
//public void ChangeEquip(Design design, EquipSlot slot, EquipItem item)
//{
// if (_items.ValidateItem(slot, item.Id, out item).Length > 0)
// return;
//
// var old = design.DesignData.Item(slot);
// if (!design.DesignData.SetItem(slot, item))
// return;
//
// Glamourer.Log.Debug(
// $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}).");
// _saveService.QueueSave(design);
// _event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot));
//}
// //
///// <summary> Change a weapon. </summary> ///// <summary> Change a weapon. </summary>
//public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item) //public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item)