diff --git a/Dalamud/Game/Gui/AgentUpdateFlag.cs b/Dalamud/Game/Gui/AgentUpdateFlag.cs new file mode 100644 index 000000000..3e2808adb --- /dev/null +++ b/Dalamud/Game/Gui/AgentUpdateFlag.cs @@ -0,0 +1,34 @@ +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +namespace Dalamud.Game.Gui; + +/// +/// Represents a flag set by the game used by agents to conditionally update their addons. +/// +[Flags] +public enum AgentUpdateFlag : byte +{ + /// Set when an inventory has been updated. + InventoryUpdate = 1 << 0, + + /// Set when a hotbar slot has been executed, or a Gearset or Macro has been changed. + ActionBarUpdate = 1 << 1, + + /// Set when the RetainerMarket inventory has been updated. + RetainerMarketInventoryUpdate = 1 << 2, + + // /// Unknown use case. + // NameplateUpdate = 1 << 3, + + /// Set when the player unlocked collectibles, contents or systems. + UnlocksUpdate = 1 << 4, + + /// Set when was called. + MainCommandEnabledStateUpdate = 1 << 5, + + /// Set when any housing inventory has been updated. + HousingInventoryUpdate = 1 << 6, + + /// Set when any content inventory has been updated. + ContentInventoryUpdate = 1 << 7, +} diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index df01dd95d..3d17aad86 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -43,6 +43,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui private readonly Hook handleImmHook; private readonly Hook setUiVisibilityHook; private readonly Hook utf8StringFromSequenceHook; + private readonly Hook raptureAtkModuleUpdateHook; [ServiceManager.ServiceConstructor] private GameGui(TargetSigScanner sigScanner) @@ -65,6 +66,10 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui this.utf8StringFromSequenceHook = Hook.FromAddress(Utf8String.Addresses.Ctor_FromSequence.Value, this.Utf8StringFromSequenceDetour); + this.raptureAtkModuleUpdateHook = Hook.FromFunctionPointerVariable( + new(&RaptureAtkModule.StaticVirtualTablePointer->Update), + this.RaptureAtkModuleUpdateDetour); + this.handleItemHoverHook.Enable(); this.handleItemOutHook.Enable(); this.handleImmHook.Enable(); @@ -72,6 +77,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui this.handleActionHoverHook.Enable(); this.handleActionOutHook.Enable(); this.utf8StringFromSequenceHook.Enable(); + this.raptureAtkModuleUpdateHook.Enable(); } // Hooked delegates @@ -88,6 +94,9 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui /// public event EventHandler? HoveredActionChanged; + /// + public event Action AgentUpdate; + /// public bool GameUiHidden { get; private set; } @@ -238,6 +247,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui this.handleActionHoverHook.Dispose(); this.handleActionOutHook.Dispose(); this.utf8StringFromSequenceHook.Dispose(); + this.raptureAtkModuleUpdateHook.Dispose(); } /// @@ -354,6 +364,21 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui return thisPtr; // this function shouldn't need to return but the original asm moves this into rax before returning so be safe? } + + private void RaptureAtkModuleUpdateDetour(RaptureAtkModule* thisPtr, float delta) + { + // The game clears the AgentUpdateFlag in the original function, but it also updates agents in it too. + // We'll make a copy of the flags, so that we can fire events after the agents have been updated. + + var agentUpdateFlag = thisPtr->AgentUpdateFlag; + + this.raptureAtkModuleUpdateHook.Original(thisPtr, delta); + + if (agentUpdateFlag != RaptureAtkModule.AgentUpdateFlags.None) + { + this.AgentUpdate.InvokeSafely((AgentUpdateFlag)agentUpdateFlag); + } + } } /// @@ -377,6 +402,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui this.gameGuiService.UiHideToggled += this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged += this.HoveredItemForward; this.gameGuiService.HoveredActionChanged += this.HoveredActionForward; + this.gameGuiService.AgentUpdate += this.AgentUpdateForward; } /// @@ -388,6 +414,9 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui /// public event EventHandler? HoveredActionChanged; + /// + public event Action AgentUpdate; + /// public bool GameUiHidden => this.gameGuiService.GameUiHidden; @@ -407,6 +436,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; this.gameGuiService.HoveredActionChanged -= this.HoveredActionForward; + this.gameGuiService.AgentUpdate -= this.AgentUpdateForward; this.UiHideToggled = null; this.HoveredItemChanged = null; @@ -458,4 +488,6 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui private void HoveredItemForward(object sender, ulong itemId) => this.HoveredItemChanged?.Invoke(sender, itemId); private void HoveredActionForward(object sender, HoveredAction hoverAction) => this.HoveredActionChanged?.Invoke(sender, hoverAction); + + private void AgentUpdateForward(AgentUpdateFlag agentUpdateFlag) => this.AgentUpdate.InvokeSafely(agentUpdateFlag); } diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 5c8ed27b0..535b84372 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -2,16 +2,14 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Dalamud.Game.Gui; using Dalamud.Game.Inventory.InventoryEventArgTypes; -using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.UI; - namespace Dalamud.Game.Inventory; /// @@ -33,7 +31,8 @@ internal class GameInventory : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - private readonly Hook raptureAtkModuleUpdateHook; + [ServiceManager.ServiceDependency] + private readonly GameGui gameGui = Service.Get(); private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; @@ -47,18 +46,9 @@ internal class GameInventory : IInternalDisposableService this.inventoryTypes = Enum.GetValues(); this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; - unsafe - { - this.raptureAtkModuleUpdateHook = Hook.FromFunctionPointerVariable( - new(&RaptureAtkModule.StaticVirtualTablePointer->Update), - this.RaptureAtkModuleUpdateDetour); - } - - this.raptureAtkModuleUpdateHook.Enable(); + this.gameGui.AgentUpdate += this.OnAgentUpdate; } - private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1); - /// void IInternalDisposableService.DisposeService() { @@ -68,7 +58,7 @@ internal class GameInventory : IInternalDisposableService this.subscribersPendingChange.Clear(); this.subscribersChanged = false; this.framework.Update -= this.OnFrameworkUpdate; - this.raptureAtkModuleUpdateHook.Dispose(); + this.gameGui.AgentUpdate -= this.OnAgentUpdate; } } @@ -319,10 +309,9 @@ internal class GameInventory : IInternalDisposableService return items; } - private unsafe void RaptureAtkModuleUpdateDetour(RaptureAtkModule* ram, float f1) + private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag) { - this.inventoriesMightBeChanged |= ram->AgentUpdateFlag != 0; - this.raptureAtkModuleUpdateHook.Original(ram, f1); + this.inventoriesMightBeChanged |= true; } /// diff --git a/Dalamud/Plugin/Services/IGameGui.cs b/Dalamud/Plugin/Services/IGameGui.cs index e2326328a..6c2e0083e 100644 --- a/Dalamud/Plugin/Services/IGameGui.cs +++ b/Dalamud/Plugin/Services/IGameGui.cs @@ -26,6 +26,12 @@ public unsafe interface IGameGui /// public event EventHandler HoveredActionChanged; + /// + /// Fired when the game sets one or more values, + /// used by agents to conditionally update their addons. + /// + event Action AgentUpdate; + /// /// Gets a value indicating whether the game UI is hidden. ///