diff --git a/Glamourer.GameData/Structs/EquipFlag.cs b/Glamourer.GameData/Structs/EquipFlag.cs
index 81590f0..eaacbac 100644
--- a/Glamourer.GameData/Structs/EquipFlag.cs
+++ b/Glamourer.GameData/Structs/EquipFlag.cs
@@ -72,4 +72,22 @@ public static class EquipFlagExtensions
EquipSlot.LFinger => EquipFlag.LFingerStain,
_ => 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,
+ };
}
diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs
index a023a83..3d71fce 100644
--- a/Glamourer/Configuration.cs
+++ b/Glamourer/Configuration.cs
@@ -20,6 +20,7 @@ public class Configuration : IPluginConfiguration, ISavable
public bool Enabled { get; set; } = true;
public bool UseRestrictedGearProtection { get; set; } = true;
public bool OpenFoldersByDefault { get; set; } = false;
+ public bool AutoRedrawEquipOnChanges { get; set; } = false;
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs
index aacf603..ba4e162 100644
--- a/Glamourer/Designs/Design.cs
+++ b/Glamourer/Designs/Design.cs
@@ -37,7 +37,7 @@ public class Design : ISavable
/// Unconditionally apply a design to a designdata.
/// Whether a redraw is required for the changes to take effect.
- public bool ApplyDesign(ref DesignData data)
+ public (bool, CustomizeFlag, EquipFlag) ApplyDesign(ref DesignData data)
{
var modelChanged = data.ModelId != DesignData.ModelId;
data.ModelId = DesignData.ModelId;
@@ -52,13 +52,16 @@ public class Design : ISavable
customizeFlags |= index.ToFlag();
}
+ EquipFlag equipFlags = 0;
foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand))
{
if (DoApplyEquip(slot))
- data.SetItem(slot, DesignData.Item(slot));
+ if (data.SetItem(slot, DesignData.Item(slot)))
+ equipFlags |= slot.ToFlag();
if (DoApplyStain(slot))
- data.SetStain(slot, DesignData.Stain(slot));
+ if (data.SetStain(slot, DesignData.Stain(slot)))
+ equipFlags |= slot.ToStainFlag();
}
if (DoApplyHatVisible())
@@ -72,7 +75,7 @@ public class Design : ISavable
if (DoApplyWetness())
data.SetIsWet(DesignData.IsWet());
- return modelChanged || customizeFlags.RequiresRedraw();
+ return (modelChanged, customizeFlags, equipFlags);
}
#endregion
diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs
index e09be27..9bd9447 100644
--- a/Glamourer/Designs/DesignData.cs
+++ b/Glamourer/Designs/DesignData.cs
@@ -63,6 +63,24 @@ public unsafe struct DesignData
// @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)
{
var index = slot.ToIndex();
diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs
index 143e0c8..b88b1cb 100644
--- a/Glamourer/Gui/Tabs/DebugTab.cs
+++ b/Glamourer/Gui/Tabs/DebugTab.cs
@@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
+using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Designs;
+using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
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(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())
+ {
+ 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)
{
if (data.ModelId == 0)
{
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))
{
var item = data.Item(slot);
@@ -1011,7 +1083,7 @@ public unsafe class DebugTab : ITab
continue;
if (_state.GetOrCreate(identifier, actors.Objects[0], out var state))
- DrawDesignData(state.ModelData);
+ DrawState(actors, state);
else
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)))
{
using var t = ImRaii.TreeNode(identifier.ToString());
- if (t)
- DrawDesignData(state.ModelData);
+ if (!t)
+ return;
+
+ DrawState(ActorData.Invalid, state);
}
}
diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs
index df624e9..c7ad35e 100644
--- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs
+++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs
@@ -1,21 +1,44 @@
using System.Numerics;
+using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Gui.Customization;
+using Glamourer.Interop;
+using Glamourer.Interop.Penumbra;
+using Glamourer.State;
+using Glamourer.Structs;
+using ImGuiNET;
using OtterGui.Raii;
+using Penumbra.Api.Enums;
+using Penumbra.Api.Helpers;
+using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.DesignTab;
public class DesignPanel
{
+ private readonly ObjectManager _objects;
private readonly DesignFileSystemSelector _selector;
private readonly DesignManager _manager;
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;
- _customizationDrawer = customizationDrawer;
- _manager = manager;
+ _selector = selector;
+ _customizationDrawer = customizationDrawer;
+ _manager = manager;
+ _objects = objects;
+ _state = state;
+ _penumbra = penumbra;
+ _changeCustomizeService = changeCustomizeService;
+ _weaponService = weaponService;
+ _updateSlot = updateSlot;
}
public void Draw()
@@ -28,6 +51,14 @@ public class DesignPanel
if (!child)
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());
}
}
diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs
index 904f446..e2d56a5 100644
--- a/Glamourer/Gui/Tabs/SettingsTab.cs
+++ b/Glamourer/Gui/Tabs/SettingsTab.cs
@@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using Dalamud.Interface;
using Glamourer.Gui.Tabs.DesignTab;
+using Glamourer.Interop.Penumbra;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
@@ -15,12 +16,14 @@ public class SettingsTab : ITab
private readonly Configuration _config;
private readonly DesignFileSystemSelector _selector;
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;
- _selector = selector;
+ _config = config;
+ _selector = selector;
_stateListener = stateListener;
+ _autoRedraw = autoRedraw;
}
public ReadOnlySpan Label
@@ -31,10 +34,14 @@ public class SettingsTab : ITab
using var child = ImRaii.Child("MainWindowChild");
if (!child)
return;
+
Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable);
Checkbox("Restricted Gear Protection",
"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);
+ 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",
"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))
diff --git a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs
new file mode 100644
index 0000000..ce4f99e
--- /dev/null
+++ b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs
@@ -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);
+ }
+}
diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs
index b092233..124363c 100644
--- a/Glamourer/Interop/Penumbra/PenumbraService.cs
+++ b/Glamourer/Interop/Penumbra/PenumbraService.cs
@@ -13,14 +13,16 @@ public unsafe class PenumbraService : IDisposable
public const int RequiredPenumbraBreakingVersion = 4;
public const int RequiredPenumbraFeatureVersion = 15;
- private readonly DalamudPluginInterface _pluginInterface;
- private readonly EventSubscriber _tooltipSubscriber;
- private readonly EventSubscriber _clickSubscriber;
- private readonly EventSubscriber _creatingCharacterBase;
- private readonly EventSubscriber _createdCharacterBase;
- private ActionSubscriber _redrawSubscriber;
- private FuncSubscriber _drawObjectInfo;
- private FuncSubscriber _cutsceneParent;
+ private readonly DalamudPluginInterface _pluginInterface;
+ private readonly EventSubscriber _tooltipSubscriber;
+ private readonly EventSubscriber _clickSubscriber;
+ private readonly EventSubscriber _creatingCharacterBase;
+ private readonly EventSubscriber _createdCharacterBase;
+ private readonly EventSubscriber _modSettingChanged;
+ private ActionSubscriber _redrawSubscriber;
+ private FuncSubscriber _drawObjectInfo;
+ private FuncSubscriber _cutsceneParent;
+ private FuncSubscriber _objectCollection;
private readonly EventSubscriber _initializedEvent;
private readonly EventSubscriber _disposedEvent;
@@ -35,6 +37,7 @@ public unsafe class PenumbraService : IDisposable
_clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi);
_createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi);
_creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi);
+ _modSettingChanged = Ipc.ModSettingChanged.Subscriber(pi);
Reattach();
}
@@ -63,6 +66,22 @@ public unsafe class PenumbraService : IDisposable
remove => _createdCharacterBase.Event -= value;
}
+ public event Action ModSettingChanged
+ {
+ add => _modSettingChanged.Event += value;
+ remove => _modSettingChanged.Event -= value;
+ }
+
+ /// Obtain the name of the collection currently assigned to the player.
+ public string GetCurrentPlayerCollection()
+ {
+ if (!Available)
+ return string.Empty;
+
+ var (valid, _, name) = _objectCollection.Invoke(0);
+ return valid ? name : string.Empty;
+ }
+
/// Obtain the game object corresponding to a draw object.
public Actor GameObjectFromDrawObject(Model drawObject)
=> Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null;
@@ -103,9 +122,11 @@ public unsafe class PenumbraService : IDisposable
_clickSubscriber.Enable();
_creatingCharacterBase.Enable();
_createdCharacterBase.Enable();
+ _modSettingChanged.Enable();
_drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface);
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
+ _objectCollection = Ipc.GetCollectionForObject.Subscriber(_pluginInterface);
Available = true;
Glamourer.Log.Debug("Glamourer attached to Penumbra.");
}
@@ -122,6 +143,7 @@ public unsafe class PenumbraService : IDisposable
_clickSubscriber.Disable();
_creatingCharacterBase.Disable();
_createdCharacterBase.Disable();
+ _modSettingChanged.Disable();
if (Available)
{
Available = false;
@@ -138,5 +160,6 @@ public unsafe class PenumbraService : IDisposable
_createdCharacterBase.Dispose();
_initializedEvent.Dispose();
_disposedEvent.Dispose();
+ _modSettingChanged.Dispose();
}
}
diff --git a/Glamourer/Interop/Structs/ActorData.cs b/Glamourer/Interop/Structs/ActorData.cs
index e7b4194..933cf0c 100644
--- a/Glamourer/Interop/Structs/ActorData.cs
+++ b/Glamourer/Interop/Structs/ActorData.cs
@@ -1,4 +1,6 @@
using System.Collections.Generic;
+using System.Linq;
+using OtterGui.Log;
namespace Glamourer.Interop.Structs;
@@ -8,7 +10,7 @@ namespace Glamourer.Interop.Structs;
public readonly struct ActorData
{
public readonly List Objects;
- public readonly string Label;
+ public readonly string Label;
public bool Valid
=> Objects.Count > 0;
@@ -16,7 +18,7 @@ public readonly struct ActorData
public ActorData(Actor actor, string label)
{
Objects = new List { actor };
- Label = label;
+ Label = label;
}
public static readonly ActorData Invalid = new(false);
@@ -24,6 +26,14 @@ public readonly struct ActorData
private ActorData(bool _)
{
Objects = new List(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);
}
}
diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs
index f515944..e13b4ac 100644
--- a/Glamourer/Interop/UpdateSlotService.cs
+++ b/Glamourer/Interop/UpdateSlotService.cs
@@ -5,6 +5,7 @@ using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
+using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Glamourer.Interop;
@@ -31,24 +32,18 @@ public unsafe class UpdateSlotService : IDisposable
{
if (!drawObject.IsCharacterBase)
return;
+
FlagSlotForUpdateInterop(drawObject, slot, data);
}
- public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor data)
- {
- if (!drawObject.IsCharacterBase)
- return;
+ public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain)
+ => UpdateSlot(drawObject, slot, armor.With(stain));
- 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)
- {
- if (!drawObject.IsHuman)
- return;
-
- FlagSlotForUpdateInterop(drawObject, slot, drawObject.GetArmor(slot).With(stain));
- }
+ => UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain);
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{
diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs
index 687ccd9..14dbc3e 100644
--- a/Glamourer/Services/ServiceManager.cs
+++ b/Glamourer/Services/ServiceManager.cs
@@ -69,7 +69,8 @@ public static class ServiceManager
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton();
+ .AddSingleton()
+ .AddSingleton();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton()
@@ -77,6 +78,7 @@ public static class ServiceManager
private static IServiceCollection AddState(this IServiceCollection services)
=> services.AddSingleton()
+ .AddSingleton()
.AddSingleton();
private static IServiceCollection AddUi(this IServiceCollection services)
diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs
index 2d963f4..1077f84 100644
--- a/Glamourer/State/ActorState.cs
+++ b/Glamourer/State/ActorState.cs
@@ -17,14 +17,20 @@ public class ActorState
HatState,
VisorState,
WeaponState,
+ ModelId,
}
public ActorIdentifier Identifier { get; internal init; }
- public DesignData ActorData;
- public DesignData ModelData;
+ /// This should always represent the unmodified state of the draw object.
+ public DesignData BaseData;
+
+ /// This should be the desired state of the draw object.
+ public DesignData ModelData;
+
+ /// This contains whether a change to the base data was made by the game, the user via manual input or through automatic application.
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)
=> Identifier = identifier;
diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs
index 9ce9516..8de89aa 100644
--- a/Glamourer/State/StateEditor.cs
+++ b/Glamourer/State/StateEditor.cs
@@ -26,6 +26,7 @@ public class StateEditor
_items = items;
}
+
public void ChangeCustomize(ActorData data, Customize customize)
{
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();
- if (idx >= 10)
- return;
-
- var armor = item.Armor();
+ var armor = state.ModelData.Armor(slot);
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
{
var mdl = actor.Model;
var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize();
var (_, resolvedItem) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
- _updateSlot.UpdateArmor(actor.Model, slot, resolvedItem);
+ _updateSlot.UpdateSlot(actor.Model, slot, resolvedItem);
}
}
diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs
index e146df2..012dde6 100644
--- a/Glamourer/State/StateListener.cs
+++ b/Glamourer/State/StateListener.cs
@@ -61,42 +61,46 @@ public class StateListener : IDisposable
Unsubscribe();
}
+ private enum UpdateState
+ {
+ NoChange,
+ Transformed,
+ Change,
+ }
+
private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr)
{
// TODO: Fixed Designs.
var actor = (Actor)actorPtr;
var identifier = actor.GetIdentifier(_actors.AwaitedService);
- if (*(int*)modelPtr != actor.AsCharacter->ModelCharaId)
- return;
-
+ var modelId = *(uint*)modelPtr;
ref var customize = ref *(Customize*)customizePtr;
if (_manager.TryGetValue(identifier, out var state))
- {
- ApplyCustomize(actor, state, ref customize);
- ApplyEquipment(actor, state, (CharacterArmor*)equipDataPtr);
- if (_config.UseRestrictedGearProtection)
- ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
- }
- else if (_config.UseRestrictedGearProtection && *(uint*)modelPtr == 0)
- {
+ switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr))
+ {
+ case UpdateState.Change: break;
+ case UpdateState.Transformed: break;
+ case UpdateState.NoChange:
+ UpdateBaseData(actor, state, customize);
+ break;
+ }
+
+ if (_config.UseRestrictedGearProtection && modelId == 0)
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
- }
}
private void OnSlotUpdating(Model model, EquipSlot slot, Ref armor, Ref returnValue)
{
// TODO handle hat state
- // TODO handle fixed designs
var actor = _penumbra.GameObjectFromDrawObject(model);
var customize = model.GetCustomize();
if (actor.Identifier(_actors.AwaitedService, out var identifier)
&& _manager.TryGetValue(identifier, out var state))
ApplyEquipmentPiece(actor, state, slot, ref armor.Value);
- var (replaced, replacedArmor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
- if (replaced)
- armor.Assign(replacedArmor);
+ if (_config.UseRestrictedGearProtection)
+ (_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
}
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref weapon)
@@ -111,7 +115,7 @@ public class StateListener : IDisposable
|| actorWeapon.Type.Value != stateItem.WeaponType
|| actorWeapon.Variant != stateItem.Variant)
{
- var oldActorItem = state.ActorData.Item(slot);
+ var oldActorItem = state.BaseData.Item(slot);
if (oldActorItem.ModelId.Value == actorWeapon.Set.Value
&& oldActorItem.WeaponType.Value == actorWeapon.Type.Value
&& oldActorItem.Variant == actorWeapon.Variant)
@@ -123,8 +127,8 @@ public class StateListener : IDisposable
else
{
var identified = _items.Identify(slot, actorWeapon.Set, actorWeapon.Type, (byte)actorWeapon.Variant,
- slot == EquipSlot.OffHand ? state.ActorData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
- state.ActorData.SetItem(slot, identified);
+ slot == EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
+ state.BaseData.SetItem(slot, identified);
if (state[slot, false] is not StateChanged.Source.Fixed)
{
state.ModelData.SetItem(slot, identified);
@@ -142,7 +146,7 @@ public class StateListener : IDisposable
var stateStain = state.ModelData.Stain(slot);
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)
{
state.ModelData.SetStain(slot, actorWeapon.Stain);
@@ -159,7 +163,7 @@ public class StateListener : IDisposable
private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize)
{
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;
foreach (var idx in Enum.GetValues())
{
@@ -201,57 +205,34 @@ public class StateListener : IDisposable
private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
{
- var actorArmor = actor.GetArmor(slot);
- if (armor.Value != actorArmor.Value)
+ var changeState = UpdateBaseData(actor, state, slot, armor);
+ if (changeState is UpdateState.Transformed)
return;
- var stateArmor = state.ModelData.Item(slot);
- if (armor.Set.Value != stateArmor.ModelId.Value || armor.Variant != stateArmor.Variant)
+ if (changeState is UpdateState.NoChange)
{
- var oldActorArmor = state.ActorData.Item(slot);
- if (oldActorArmor.ModelId.Value == actorArmor.Set.Value && oldActorArmor.Variant == actorArmor.Variant)
- {
- armor.Set = stateArmor.ModelId;
- armor.Variant = stateArmor.Variant;
- }
- else
- {
- var identified = _items.Identify(slot, actorArmor.Set, actorArmor.Variant);
- state.ActorData.SetItem(slot, identified);
- if (state[slot, false] is not StateChanged.Source.Fixed)
- {
- state.ModelData.SetItem(slot, identified);
- state[slot, false] = StateChanged.Source.Game;
- }
- else
- {
- armor.Set = stateArmor.ModelId;
- armor.Variant = stateArmor.Variant;
- }
- }
+ armor = state.ModelData.Armor(slot);
}
-
- var stateStain = state.ModelData.Stain(slot);
- if (armor.Stain.Value != stateStain.Value)
+ else
{
- var oldActorStain = state.ActorData.Stain(slot);
- if (oldActorStain.Value == actorArmor.Stain.Value)
+ var modelArmor = state.ModelData.Armor(slot);
+ if (armor.Value == modelArmor.Value)
+ return;
+
+ if (state[slot, false] is StateChanged.Source.Fixed)
{
- armor.Stain = stateStain;
+ armor.Set = modelArmor.Set;
+ armor.Variant = modelArmor.Variant;
}
else
{
- 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;
- }
+ _manager.ChangeEquip(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game);
}
+
+ if (state[slot, true] is StateChanged.Source.Fixed)
+ armor.Stain = modelArmor.Stain;
+ else
+ _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
}
}
@@ -280,4 +261,81 @@ public class StateListener : IDisposable
_slotUpdating.Unsubscribe(OnSlotUpdating);
_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;
+ }
}
diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs
index c20b47c..d3bb9f6 100644
--- a/Glamourer/State/StateManager.cs
+++ b/Glamourer/State/StateManager.cs
@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Security.Cryptography;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
@@ -11,6 +12,7 @@ using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.Structs;
+using OtterGui.Log;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@@ -24,13 +26,15 @@ public class StateManager : IReadOnlyDictionary
private readonly CustomizationService _customizations;
private readonly VisorService _visor;
private readonly StateChanged _event;
+ private readonly ObjectManager _objects;
+ private readonly StateEditor _editor;
private readonly PenumbraService _penumbra;
-
+
private readonly Dictionary _states = new();
public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event,
- PenumbraService penumbra)
+ PenumbraService penumbra, ObjectManager objects, StateEditor editor)
{
_actors = actors;
_items = items;
@@ -38,7 +42,8 @@ public class StateManager : IReadOnlyDictionary
_visor = visor;
_event = @event;
_penumbra = penumbra;
-
+ _objects = objects;
+ _editor = editor;
}
public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state)
@@ -55,7 +60,7 @@ public class StateManager : IReadOnlyDictionary
state = new ActorState(identifier)
{
ModelData = designData,
- ActorData = designData
+ BaseData = designData,
};
_states.Add(identifier, state);
return true;
@@ -115,7 +120,7 @@ public class StateManager : IReadOnlyDictionary
UpdateEquip(state, slot, model.GetArmor(slot));
state.ModelData.Customize = model.GetCustomize();
- var (_, _, main, off) = model.GetWeapons(actor);
+ var (_, _, main, off) = model.GetWeapons(actor);
UpdateWeapon(state, EquipSlot.MainHand, main);
UpdateWeapon(state, EquipSlot.OffHand, off);
state.ModelData.SetVisor(_visor.GetVisorState(model));
@@ -165,9 +170,9 @@ public class StateManager : IReadOnlyDictionary
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);
return ret;
}
@@ -242,6 +247,94 @@ public class StateManager : IReadOnlyDictionary
$"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));
}
+
+ 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));
+ }
+
//
///// Change whether to apply a specific customize value.
//public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
@@ -255,21 +348,7 @@ public class StateManager : IReadOnlyDictionary
// _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx);
//}
//
- ///// Change a non-weapon equipment piece.
- //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));
- //}
+
//
///// Change a weapon.
//public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item)