diff --git a/Glamourer/Events/EquipmentLoading.cs b/Glamourer/Events/EquipmentLoading.cs
new file mode 100644
index 0000000..93d2656
--- /dev/null
+++ b/Glamourer/Events/EquipmentLoading.cs
@@ -0,0 +1,31 @@
+using System;
+using Glamourer.Interop.Structs;
+using OtterGui.Classes;
+using Penumbra.GameData.Enums;
+using Penumbra.GameData.Structs;
+
+namespace Glamourer.Events;
+
+///
+/// Triggered when a game object updates an equipment piece in its model data.
+///
+/// - Parameter is the character updating.
+/// - Parameter is the equipment slot changed.
+/// - Parameter is the model values to change the equipment piece to.
+///
+///
+public sealed class EquipmentLoading : EventWrapper, EquipmentLoading.Priority>
+{
+ public enum Priority
+ {
+ ///
+ StateListener = 0,
+ }
+
+ public EquipmentLoading()
+ : base(nameof(EquipmentLoading))
+ { }
+
+ public void Invoke(Actor actor, EquipSlot slot, CharacterArmor armor)
+ => Invoke(this, actor, slot, armor);
+}
diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs
index e13b4ac..f062a4f 100644
--- a/Glamourer/Interop/UpdateSlotService.cs
+++ b/Glamourer/Interop/UpdateSlotService.cs
@@ -1,32 +1,33 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
+using FFXIVClientStructs.FFXIV.Client.Game.Character;
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;
public unsafe class UpdateSlotService : IDisposable
{
- public readonly SlotUpdating Event;
+ public readonly SlotUpdating SlotUpdatingEvent;
+ public readonly EquipmentLoading EquipmentLoadingEvent;
- public UpdateSlotService(SlotUpdating slotUpdating)
+ public UpdateSlotService(SlotUpdating slotUpdating, EquipmentLoading equipmentLoadingEvent)
{
- Event = slotUpdating;
+ SlotUpdatingEvent = slotUpdating;
+ EquipmentLoadingEvent = equipmentLoadingEvent;
SignatureHelper.Initialise(this);
_flagSlotForUpdateHook.Enable();
+ _loadEquipmentHook.Enable();
}
public void Dispose()
- => _flagSlotForUpdateHook.Dispose();
-
- private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data);
-
- [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))]
- private readonly Hook _flagSlotForUpdateHook = null!;
+ {
+ _flagSlotForUpdateHook.Dispose();
+ _loadEquipmentHook.Dispose();
+ }
public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data)
{
@@ -45,14 +46,34 @@ public unsafe class UpdateSlotService : IDisposable
public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain)
=> UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain);
+ private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data);
+
+ [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))]
+ private readonly Hook _flagSlotForUpdateHook = null!;
+
+ private delegate void LoadEquipmentDelegateIntern(DrawDataContainer* drawDataContainer, uint slotIdx, CharacterArmor data, bool force);
+
+ // TODO: use client structs.
+ [Signature("E8 ?? ?? ?? ?? 41 B5 ?? FF C6", DetourName = nameof(LoadEquipmentDetour))]
+ private readonly Hook _loadEquipmentHook = null!;
+
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{
var slot = slotIdx.ToEquipSlot();
var returnValue = ulong.MaxValue;
- Event.Invoke(drawObject, slot, ref *data, ref returnValue);
+ SlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue);
+ Glamourer.Log.Information($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue}).");
return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue;
}
+ private void LoadEquipmentDetour(DrawDataContainer* drawDataContainer, uint slotIdx, CharacterArmor data, bool force)
+ {
+ var slot = slotIdx.ToEquipSlot();
+ EquipmentLoadingEvent.Invoke(drawDataContainer->Parent, slot, data);
+ Glamourer.Log.Information($"[LoadEquipment] Called with 0x{(ulong)drawDataContainer:X} for slot {slot} with {data} ({force}).");
+ _loadEquipmentHook.Original(drawDataContainer, slotIdx, data, force);
+ }
+
private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor)
=> _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
}
diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs
index 7a14f46..6a49232 100644
--- a/Glamourer/Services/ServiceManager.cs
+++ b/Glamourer/Services/ServiceManager.cs
@@ -61,6 +61,7 @@ public static class ServiceManager
private static IServiceCollection AddEvents(this IServiceCollection services)
=> services.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs
index c53b4c7..ebf7083 100644
--- a/Glamourer/State/StateListener.cs
+++ b/Glamourer/State/StateListener.cs
@@ -6,6 +6,7 @@ using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using OtterGui.Classes;
+using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@@ -25,6 +26,7 @@ public class StateListener : IDisposable
private readonly ItemManager _items;
private readonly PenumbraService _penumbra;
private readonly SlotUpdating _slotUpdating;
+ private readonly EquipmentLoading _equipmentLoading;
private readonly WeaponLoading _weaponLoading;
private readonly HeadGearVisibilityChanged _headGearVisibility;
private readonly VisorStateChanged _visorState;
@@ -41,7 +43,8 @@ public class StateListener : IDisposable
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config,
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
- HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans)
+ HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans,
+ EquipmentLoading equipmentLoading)
{
_manager = manager;
_items = items;
@@ -56,6 +59,7 @@ public class StateListener : IDisposable
_autoDesignApplier = autoDesignApplier;
_funModule = funModule;
_humans = humans;
+ _equipmentLoading = equipmentLoading;
if (Enabled)
Subscribe();
@@ -159,6 +163,42 @@ public class StateListener : IDisposable
(_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
}
+ ///
+ /// The game object does not actually invoke changes when the model id is identical,
+ /// so we need to handle that case too.
+ ///
+ private void OnEquipmentLoading(Actor actor, EquipSlot slot, CharacterArmor armor)
+ {
+ if (armor != actor.GetArmor(slot))
+ return;
+
+ if (!actor.Identifier(_actors.AwaitedService, out var identifier)
+ || !_manager.TryGetValue(identifier, out var state) || !state.BaseData.IsHuman)
+ return;
+
+ if (state.ModelData.Armor(slot) == armor)
+ return;
+
+ var setItem = state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc;
+ var setStain = state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc;
+ switch (setItem, setStain)
+ {
+ case (true, true):
+ _manager.ChangeEquip(state, slot, state.BaseData.Item(slot), state.BaseData.Stain(slot), StateChanged.Source.Manual);
+ state[slot, false] = StateChanged.Source.Game;
+ state[slot, true] = StateChanged.Source.Game;
+ break;
+ case (true, false):
+ _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Manual);
+ state[slot, false] = StateChanged.Source.Game;
+ break;
+ case (false, true):
+ _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Manual);
+ state[slot, true] = StateChanged.Source.Game;
+ break;
+ }
+ }
+
///
/// A game object loads a new weapon.
/// Update base data, apply or update model data.
@@ -179,24 +219,14 @@ public class StateListener : IDisposable
case UpdateState.Transformed: break;
case UpdateState.Change:
if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
- {
- state.ModelData.SetItem(slot, state.BaseData.Item(slot));
- state[slot, false] = StateChanged.Source.Game;
- }
+ _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game);
else
- {
apply = true;
- }
if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
- {
- state.ModelData.SetStain(slot, state.BaseData.Stain(slot));
- state[slot, true] = StateChanged.Source.Game;
- }
+ _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
else
- {
apply = true;
- }
break;
case UpdateState.NoChange:
@@ -254,24 +284,14 @@ public class StateListener : IDisposable
case UpdateState.Change:
var apply = false;
if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
- {
- state.ModelData.SetItem(slot, state.BaseData.Item(slot));
- state[slot, false] = StateChanged.Source.Game;
- }
+ _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game);
else
- {
apply = true;
- }
if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
- {
- state.ModelData.SetStain(slot, state.BaseData.Stain(slot));
- state[slot, true] = StateChanged.Source.Game;
- }
+ _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
else
- {
apply = true;
- }
if (apply)
armor = state.ModelData.Armor(slot);
@@ -464,6 +484,7 @@ public class StateListener : IDisposable
{
_penumbra.CreatingCharacterBase += OnCreatingCharacterBase;
_slotUpdating.Subscribe(OnSlotUpdating, SlotUpdating.Priority.StateListener);
+ _equipmentLoading.Subscribe(OnEquipmentLoading, EquipmentLoading.Priority.StateListener);
_weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener);
_visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener);
_headGearVisibility.Subscribe(OnHeadGearVisibilityChange, HeadGearVisibilityChanged.Priority.StateListener);
@@ -474,6 +495,7 @@ public class StateListener : IDisposable
{
_penumbra.CreatingCharacterBase -= OnCreatingCharacterBase;
_slotUpdating.Unsubscribe(OnSlotUpdating);
+ _equipmentLoading.Unsubscribe(OnEquipmentLoading);
_weaponLoading.Unsubscribe(OnWeaponLoading);
_visorState.Unsubscribe(OnVisorChange);
_headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange);