diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 4da7b6857..51ab6b1d2 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,18 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui /// public event EventHandler? HoveredActionChanged; + /// + public event Action InventoryUpdate; + + /// + public event Action ActionBarUpdate; + + /// + public event Action UnlocksUpdate; + + /// + public event Action MainCommandEnabledStateUpdate; + /// public bool GameUiHidden { get; private set; } @@ -238,6 +256,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui this.handleActionHoverHook.Dispose(); this.handleActionOutHook.Dispose(); this.utf8StringFromSequenceHook.Dispose(); + this.raptureAtkModuleUpdateHook.Dispose(); } /// @@ -362,6 +381,34 @@ 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) + { + if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.InventoryUpdate) || + agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.RetainerMarketInventoryUpdate) || + agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.HousingInventoryUpdate) || + agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.ContentInventoryUpdate)) + this.InventoryUpdate.InvokeSafely(); + + if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.ActionBarUpdate)) + this.ActionBarUpdate.InvokeSafely(); + + if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.UnlocksUpdate)) + this.UnlocksUpdate.InvokeSafely(); + + if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.MainCommandEnabledStateUpdate)) + this.MainCommandEnabledStateUpdate.InvokeSafely(); + } + } } /// @@ -385,6 +432,10 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui this.gameGuiService.UiHideToggled += this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged += this.HoveredItemForward; this.gameGuiService.HoveredActionChanged += this.HoveredActionForward; + this.gameGuiService.InventoryUpdate += this.InventoryUpdateForward; + this.gameGuiService.ActionBarUpdate += this.ActionBarUpdateForward; + this.gameGuiService.UnlocksUpdate += this.UnlocksUpdateForward; + this.gameGuiService.MainCommandEnabledStateUpdate += this.MainCommandEnabledStateUpdateForward; } /// @@ -396,6 +447,18 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui /// public event EventHandler? HoveredActionChanged; + /// + public event Action InventoryUpdate; + + /// + public event Action ActionBarUpdate; + + /// + public event Action UnlocksUpdate; + + /// + public event Action MainCommandEnabledStateUpdate; + /// public bool GameUiHidden => this.gameGuiService.GameUiHidden; @@ -415,6 +478,10 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; this.gameGuiService.HoveredActionChanged -= this.HoveredActionForward; + this.gameGuiService.InventoryUpdate -= this.InventoryUpdateForward; + this.gameGuiService.ActionBarUpdate -= this.ActionBarUpdateForward; + this.gameGuiService.UnlocksUpdate -= this.UnlocksUpdateForward; + this.gameGuiService.MainCommandEnabledStateUpdate -= this.MainCommandEnabledStateUpdateForward; this.UiHideToggled = null; this.HoveredItemChanged = null; @@ -466,4 +533,12 @@ 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 InventoryUpdateForward() => this.InventoryUpdate.InvokeSafely(); + + private void ActionBarUpdateForward() => this.ActionBarUpdate.InvokeSafely(); + + private void UnlocksUpdateForward() => this.UnlocksUpdate.InvokeSafely(); + + private void MainCommandEnabledStateUpdateForward() => this.MainCommandEnabledStateUpdate.InvokeSafely(); } diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 5c8ed27b0..e2e3c1ec2 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.InventoryUpdate += this.OnInventoryUpdate; } - 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.InventoryUpdate -= this.OnInventoryUpdate; } } @@ -319,10 +309,9 @@ internal class GameInventory : IInternalDisposableService return items; } - private unsafe void RaptureAtkModuleUpdateDetour(RaptureAtkModule* ram, float f1) + private void OnInventoryUpdate() { - 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..fc37c14b7 100644 --- a/Dalamud/Plugin/Services/IGameGui.cs +++ b/Dalamud/Plugin/Services/IGameGui.cs @@ -26,6 +26,27 @@ public unsafe interface IGameGui /// public event EventHandler HoveredActionChanged; + /// + /// Event that is fired when the inventory has been updated. + /// + event Action InventoryUpdate; + + /// + /// Fired when the action bar needs to be updated, e.g. after changing Class/Job, + /// updating Gear Sets, modifying Macros, or executing a hotbar slot. + /// + event Action ActionBarUpdate; + + /// + /// Event that is fired when collectibles, content or systems were unlocked. + /// + event Action UnlocksUpdate; + + /// + /// Event that is fired when the enable state of MainCommands has been updated. + /// + event Action MainCommandEnabledStateUpdate; + /// /// Gets a value indicating whether the game UI is hidden. ///