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);