More state-keeping for when the game object changes an item but with identical model information...

This commit is contained in:
Ottermandias 2023-07-18 00:53:47 +02:00
parent 9c42872456
commit c6d24d83da
4 changed files with 111 additions and 36 deletions

View file

@ -0,0 +1,31 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a game object updates an equipment piece in its model data.
/// <list type="number">
/// <item>Parameter is the character updating. </item>
/// <item>Parameter is the equipment slot changed. </item>
/// <item>Parameter is the model values to change the equipment piece to. </item>
/// </list>
/// </summary>
public sealed class EquipmentLoading : EventWrapper<Action<Actor, EquipSlot, CharacterArmor>, EquipmentLoading.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnEquipmentLoading"/>
StateListener = 0,
}
public EquipmentLoading()
: base(nameof(EquipmentLoading))
{ }
public void Invoke(Actor actor, EquipSlot slot, CharacterArmor armor)
=> Invoke(this, actor, slot, armor);
}

View file

@ -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<FlagSlotForUpdateDelegateIntern> _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<FlagSlotForUpdateDelegateIntern> _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<LoadEquipmentDelegateIntern> _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);
}

View file

@ -61,6 +61,7 @@ public static class ServiceManager
private static IServiceCollection AddEvents(this IServiceCollection services)
=> services.AddSingleton<VisorStateChanged>()
.AddSingleton<SlotUpdating>()
.AddSingleton<EquipmentLoading>()
.AddSingleton<DesignChanged>()
.AddSingleton<AutomationChanged>()
.AddSingleton<StateChanged>()

View file

@ -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);
}
/// <summary>
/// The game object does not actually invoke changes when the model id is identical,
/// so we need to handle that case too.
/// </summary>
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;
}
}
/// <summary>
/// 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);