diff --git a/Dalamud/Game/AddonEventManager/AddonCursorType.cs b/Dalamud/Game/AddonEventManager/AddonCursorType.cs index 8ba3a901b..57d58c60c 100644 --- a/Dalamud/Game/AddonEventManager/AddonCursorType.cs +++ b/Dalamud/Game/AddonEventManager/AddonCursorType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// Reimplementation of CursorType. diff --git a/Dalamud/Game/AddonEventManager/AddonEventEntry.cs b/Dalamud/Game/AddonEventManager/AddonEventEntry.cs new file mode 100644 index 000000000..48c3feb24 --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventEntry.cs @@ -0,0 +1,61 @@ +using System; + +using Dalamud.Memory; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon; + +/// +/// This class represents a registered event that a plugin registers with a native ui node. +/// Contains all necessary information to track and clean up events automatically. +/// +internal unsafe class AddonEventEntry +{ + /// + /// Name of an invalid addon. + /// + public const string InvalidAddonName = "NullAddon"; + + private string? addonName; + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + required public nint Addon { get; init; } + + /// + /// Gets the name of the addon this args referrers to. + /// + public string AddonName => this.Addon == nint.Zero ? InvalidAddonName : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + + /// + /// Gets the pointer to the event source. + /// + required public nint Node { get; init; } + + /// + /// Gets the handler that gets called when this event is triggered. + /// + required public IAddonEventManager.AddonEventHandler Handler { get; init; } + + /// + /// Gets the unique id for this event. + /// + required public uint ParamKey { get; init; } + + /// + /// Gets the event type for this event. + /// + required public AddonEventType EventType { get; init; } + + /// + /// Gets the event handle for this event. + /// + required internal IAddonEventHandle Handle { get; init; } + + /// + /// Gets the formatted log string for this AddonEventEntry. + /// + internal string LogString => $"ParamKey: {this.ParamKey}, Addon: {this.AddonName}, Event: {this.EventType}, GUID: {this.Handle.EventGuid}"; +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventHandle.cs b/Dalamud/Game/AddonEventManager/AddonEventHandle.cs new file mode 100644 index 000000000..48abba9a0 --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventHandle.cs @@ -0,0 +1,21 @@ +using System; + +namespace Dalamud.Game.Addon; + +/// +/// Class that represents a addon event handle. +/// +public class AddonEventHandle : IAddonEventHandle +{ + /// + public uint ParamKey { get; init; } + + /// + public string AddonName { get; init; } = "NullAddon"; + + /// + public AddonEventType EventType { get; init; } + + /// + public Guid EventGuid { get; init; } +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventListener.cs b/Dalamud/Game/AddonEventManager/AddonEventListener.cs index cb0aa1502..6f7c55c4c 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventListener.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventListener.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// Event listener class for managing custom events. @@ -39,6 +39,11 @@ internal unsafe class AddonEventListener : IDisposable /// Event Data. /// Unknown Parameter. public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown); + + /// + /// Gets the address of this listener. + /// + public nint Address => (nint)this.eventListener; /// public void Dispose() diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 4718d4800..dfc037e23 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -1,30 +1,41 @@ using System; using System.Collections.Generic; +using System.Linq; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// Service provider for addon event management. /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEventManager +internal unsafe class AddonEventManager : IDisposable, IServiceType { + /// + /// PluginName for Dalamud Internal use. + /// + public const string DalamudInternalKey = "Dalamud.Internal"; + private static readonly ModuleLog Log = new("AddonEventManager"); + [ServiceManager.ServiceDependency] + private readonly AddonLifecycle addonLifecycle = Service.Get(); + + private readonly AddonLifecycleEventListener finalizeEventListener; + private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; - private readonly AddonEventListener eventListener; - private readonly Dictionary eventHandlers; + private readonly List pluginEventControllers; private AddonCursorType? cursorOverride; @@ -34,64 +45,108 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); - this.eventHandlers = new Dictionary(); - this.eventListener = new AddonEventListener(this.DalamudAddonEventHandler); + this.pluginEventControllers = new List + { + new(DalamudInternalKey), // Create entry for Dalamud's Internal Use. + }; this.cursorOverride = null; this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); + + this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize); + this.addonLifecycle.RegisterListener(this.finalizeEventListener); } private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); - /// - public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) - { - if (!this.eventHandlers.ContainsKey(eventId)) - { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - var addon = (AtkUnitBase*)atkUnitBase; - - this.eventHandlers.Add(eventId, eventHandler); - this.eventListener.RegisterEvent(addon, node, type, eventId); - } - else - { - Log.Warning($"Attempted to register already registered eventId: {eventId}"); - } - } - - /// - public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType) - { - if (this.eventHandlers.ContainsKey(eventId)) - { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - - this.eventListener.UnregisterEvent(node, type, eventId); - this.eventHandlers.Remove(eventId); - } - else - { - Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); - } - } - /// public void Dispose() { this.onUpdateCursor.Dispose(); - this.eventListener.Dispose(); - this.eventHandlers.Clear(); + + foreach (var pluginEventController in this.pluginEventControllers) + { + pluginEventController.Dispose(); + } + + this.addonLifecycle.UnregisterListener(this.finalizeEventListener); + } + + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// Unique ID for this plugin. + /// The parent addon for this event. + /// The node that will trigger this event. + /// The event type for this event. + /// The handler to call when event is triggered. + /// IAddonEventHandle used to remove the event. + internal IAddonEventHandle? AddEvent(string pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + { + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) + { + return eventController.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler); + } + + Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); + return null; + } + + /// + /// Unregisters an event handler with the specified event id and event type. + /// + /// Unique ID for this plugin. + /// The Unique Id for this event. + internal void RemoveEvent(string pluginId, IAddonEventHandle eventHandle) + { + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) + { + eventController.RemoveEvent(eventHandle); + } + else + { + Log.Verbose($"Unable to locate controller for {pluginId}. No event was removed."); + } } - /// - public void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; + /// + /// Force the game cursor to be the specified cursor. + /// + /// Which cursor to use. + internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; - /// - public void ResetCursor() => this.cursorOverride = null; + /// + /// Un-forces the game cursor. + /// + internal void ResetCursor() => this.cursorOverride = null; + + /// + /// Adds a new managed event controller if one doesn't already exist for this pluginId. + /// + /// Unique ID for this plugin. + internal void AddPluginEventController(string pluginId) + { + if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId)) + { + Log.Verbose($"Creating new PluginEventController for: {pluginId}"); + this.pluginEventControllers.Add(new PluginEventController(pluginId)); + } + } + + /// + /// Removes an existing managed event controller for the specified plugin. + /// + /// Unique ID for this plugin. + internal void RemovePluginEventController(string pluginId) + { + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller) + { + Log.Verbose($"Removing PluginEventController for: {pluginId}"); + this.pluginEventControllers.Remove(controller); + controller.Dispose(); + } + } [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() @@ -99,6 +154,22 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent this.onUpdateCursor.Enable(); } + /// + /// When an addon finalizes, check it for any registered events, and unregister them. + /// + /// Event type that triggered this call. + /// Addon that triggered this call. + private void OnAddonFinalize(AddonEvent eventType, AddonArgs addonInfo) + { + // It shouldn't be possible for this event to be anything other than PreFinalize. + if (eventType != AddonEvent.PreFinalize) return; + + foreach (var pluginList in this.pluginEventControllers) + { + pluginList.RemoveForAddon(addonInfo.AddonName); + } + } + private nint UpdateCursorDetour(RaptureAtkModule* module) { try @@ -123,22 +194,6 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent return this.onUpdateCursor!.Original(module); } - - private void DalamudAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) - { - if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) - { - try - { - // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler - handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); - } - catch (Exception exception) - { - Log.Error(exception, "Exception in DalamudAddonEventHandler custom event invoke."); - } - } - } } /// @@ -150,25 +205,24 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager +internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager { - private static readonly ModuleLog Log = new("AddonEventManager"); - [ServiceManager.ServiceDependency] - private readonly AddonEventManager baseEventManager = Service.Get(); + private readonly AddonEventManager eventManagerService = Service.Get(); - private readonly AddonEventListener eventListener; - private readonly Dictionary eventHandlers; + private readonly LocalPlugin plugin; private bool isForcingCursor; /// /// Initializes a new instance of the class. /// - public AddonEventManagerPluginScoped() + /// Plugin info for the plugin that requested this service. + public AddonEventManagerPluginScoped(LocalPlugin plugin) { - this.eventHandlers = new Dictionary(); - this.eventListener = new AddonEventListener(this.PluginAddonEventHandler); + this.plugin = plugin; + + this.eventManagerService.AddPluginEventController(plugin.Manifest.WorkingPluginId.ToString()); } /// @@ -177,54 +231,26 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. if (this.isForcingCursor) { - this.baseEventManager.ResetCursor(); + this.eventManagerService.ResetCursor(); } - this.eventListener.Dispose(); - this.eventHandlers.Clear(); + this.eventManagerService.RemovePluginEventController(this.plugin.Manifest.WorkingPluginId.ToString()); } /// - public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) - { - if (!this.eventHandlers.ContainsKey(eventId)) - { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - var addon = (AtkUnitBase*)atkUnitBase; + public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + => this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId.ToString(), atkUnitBase, atkResNode, eventType, eventHandler); - this.eventHandlers.Add(eventId, eventHandler); - this.eventListener.RegisterEvent(addon, node, type, eventId); - } - else - { - Log.Warning($"Attempted to register already registered eventId: {eventId}"); - } - } - /// - public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType) - { - if (this.eventHandlers.ContainsKey(eventId)) - { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - - this.eventListener.UnregisterEvent(node, type, eventId); - this.eventHandlers.Remove(eventId); - } - else - { - Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); - } - } + public void RemoveEvent(IAddonEventHandle eventHandle) + => this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventHandle); /// public void SetCursor(AddonCursorType cursor) { this.isForcingCursor = true; - this.baseEventManager.SetCursor(cursor); + this.eventManagerService.SetCursor(cursor); } /// @@ -232,22 +258,6 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, { this.isForcingCursor = false; - this.baseEventManager.ResetCursor(); - } - - private void PluginAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) - { - if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) - { - try - { - // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler - handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); - } - catch (Exception exception) - { - Log.Error(exception, "Exception in PluginAddonEventHandler custom event invoke."); - } - } + this.eventManagerService.ResetCursor(); } } diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs index ba1c07db8..71a6093bb 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// AddonEventManager memory address resolver. diff --git a/Dalamud/Game/AddonEventManager/AddonEventType.cs b/Dalamud/Game/AddonEventManager/AddonEventType.cs index eef9763ff..74f35c257 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventType.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// Reimplementation of AtkEventType. diff --git a/Dalamud/Game/AddonEventManager/IAddonEventHandle.cs b/Dalamud/Game/AddonEventManager/IAddonEventHandle.cs new file mode 100644 index 000000000..3b2c5c3ae --- /dev/null +++ b/Dalamud/Game/AddonEventManager/IAddonEventHandle.cs @@ -0,0 +1,29 @@ +using System; + +namespace Dalamud.Game.Addon; + +/// +/// Interface representing the data used for managing AddonEvents. +/// +public interface IAddonEventHandle +{ + /// + /// Gets the param key associated with this event. + /// + public uint ParamKey { get; init; } + + /// + /// Gets the name of the addon that this event was attached to. + /// + public string AddonName { get; init; } + + /// + /// Gets the event type associated with this handle. + /// + public AddonEventType EventType { get; init; } + + /// + /// Gets the unique ID for this handle. + /// + public Guid EventGuid { get; init; } +} diff --git a/Dalamud/Game/AddonEventManager/PluginEventController.cs b/Dalamud/Game/AddonEventManager/PluginEventController.cs new file mode 100644 index 000000000..b66bbc99e --- /dev/null +++ b/Dalamud/Game/AddonEventManager/PluginEventController.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Game.Gui; +using Dalamud.Logging.Internal; +using Dalamud.Memory; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon; + +/// +/// Class to manage creating and cleaning up events per-plugin. +/// +internal unsafe class PluginEventController : IDisposable +{ + private static readonly ModuleLog Log = new("AddonEventManager"); + + /// + /// Initializes a new instance of the class. + /// + /// The Unique ID for this plugin. + public PluginEventController(string pluginId) + { + this.PluginId = pluginId; + + this.EventListener = new AddonEventListener(this.PluginEventListHandler); + } + + /// + /// Gets the unique ID for this PluginEventList. + /// + public string PluginId { get; init; } + + private AddonEventListener EventListener { get; init; } + + private List Events { get; } = new(); + + /// + /// Adds a tracked event. + /// + /// The Parent addon for the event. + /// The Node for the event. + /// The Event Type. + /// The delegate to call when invoking this event. + /// IAddonEventHandle used to remove the event. + public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler) + { + var node = (AtkResNode*)atkResNode; + var addon = (AtkUnitBase*)atkUnitBase; + var eventType = (AtkEventType)atkEventType; + var eventId = this.GetNextParamKey(); + var eventGuid = Guid.NewGuid(); + + var eventHandle = new AddonEventHandle + { + AddonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name), + ParamKey = eventId, + EventType = atkEventType, + EventGuid = eventGuid, + }; + + var eventEntry = new AddonEventEntry + { + Addon = atkUnitBase, + Handler = handler, + Node = atkResNode, + EventType = atkEventType, + ParamKey = eventId, + Handle = eventHandle, + }; + + Log.Verbose($"Adding Event. {eventEntry.LogString}"); + this.EventListener.RegisterEvent(addon, node, eventType, eventId); + this.Events.Add(eventEntry); + + return eventHandle; + } + + /// + /// Removes a tracked event, also attempts to un-attach the event from native. + /// + /// Unique ID of the event to remove. + public void RemoveEvent(IAddonEventHandle handle) + { + if (this.Events.FirstOrDefault(registeredEvent => registeredEvent.Handle == handle) is not { } targetEvent) return; + + Log.Verbose($"Removing Event. {targetEvent.LogString}"); + this.TryRemoveEventFromNative(targetEvent); + this.Events.Remove(targetEvent); + } + + /// + /// Removes all events attached to the specified addon. + /// + /// Addon name to remove events from. + public void RemoveForAddon(string addonName) + { + if (this.Events.Where(entry => entry.AddonName == addonName).ToList() is { Count: not 0 } events) + { + Log.Verbose($"Addon: {addonName} is Finalizing, removing {events.Count} events."); + + foreach (var registeredEvent in events) + { + this.RemoveEvent(registeredEvent.Handle); + } + } + } + + /// + public void Dispose() + { + foreach (var registeredEvent in this.Events.ToList()) + { + this.RemoveEvent(registeredEvent.Handle); + } + + this.EventListener.Dispose(); + } + + private uint GetNextParamKey() + { + for (var i = 0u; i < uint.MaxValue; ++i) + { + if (this.Events.All(registeredEvent => registeredEvent.ParamKey != i)) return i; + } + + throw new OverflowException($"uint.MaxValue number of ParamKeys used for {this.PluginId}"); + } + + /// + /// Attempts to remove a tracked event from native UI. + /// This method performs several safety checks to only remove events from a still active addon. + /// If any of these checks fail, it likely means the native UI already cleaned up the event, and we don't have to worry about them. + /// + /// Event entry to remove. + private void TryRemoveEventFromNative(AddonEventEntry eventEntry) + { + // Is the eventEntry addon valid? + if (eventEntry.AddonName is AddonEventEntry.InvalidAddonName) return; + + // Is an addon with the same name active? + var currentAddonPointer = Service.Get().GetAddonByName(eventEntry.AddonName); + if (currentAddonPointer == nint.Zero) return; + + // Is our stored addon pointer the same as the active addon pointer? + if (currentAddonPointer != eventEntry.Addon) return; + + // Does this addon contain the node this event is for? (by address) + var atkUnitBase = (AtkUnitBase*)currentAddonPointer; + var nodeFound = false; + foreach (var index in Enumerable.Range(0, atkUnitBase->UldManager.NodeListCount)) + { + var node = atkUnitBase->UldManager.NodeList[index]; + + // If this node matches our node, then we know our node is still valid. + if (node is not null && (nint)node == eventEntry.Node) + { + nodeFound = true; + } + } + + // If we didn't find the node, we can't remove the event. + if (!nodeFound) return; + + // Does the node have a registered event matching the parameters we have? + var atkResNode = (AtkResNode*)eventEntry.Node; + var eventType = (AtkEventType)eventEntry.EventType; + var currentEvent = atkResNode->AtkEventManager.Event; + var eventFound = false; + while (currentEvent is not null) + { + var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey; + var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address; + var eventTypeMatches = currentEvent->Type == eventType; + + if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches) + { + eventFound = true; + break; + } + + // Move to the next event. + currentEvent = currentEvent->NextEvent; + } + + // If we didn't find the event, we can't remove the event. + if (!eventFound) return; + + // We have a valid addon, valid node, valid event, and valid key. + this.EventListener.UnregisterEvent(atkResNode, eventType, eventEntry.ParamKey); + } + + private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) + { + try + { + if (eventData is null) return; + if (this.Events.FirstOrDefault(handler => handler.ParamKey == eventParam) is not { } eventInfo) return; + + // We stored the AtkUnitBase* in EventData->Node, and EventData->Target contains the node that triggered the event. + eventInfo.Handler.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); + } + catch (Exception exception) + { + Log.Error(exception, "Exception in PluginEventList custom event invoke."); + } + } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs new file mode 100644 index 000000000..949d3fde9 --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs @@ -0,0 +1,46 @@ +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon; + +/// +/// Base class for AddonLifecycle AddonArgTypes. +/// +public abstract unsafe class AddonArgs +{ + /// + /// Constant string representing the name of an addon that is invalid. + /// + public const string InvalidAddon = "NullAddon"; + + private string? addonName; + + /// + /// Gets the name of the addon this args referrers to. + /// + public string AddonName => this.GetAddonName(); + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + public nint Addon { get; init; } + + /// + /// Gets the type of these args. + /// + public abstract AddonArgsType Type { get; } + + /// + /// Helper method for ensuring the name of the addon is valid. + /// + /// The name of the addon for this object. when invalid. + private string GetAddonName() + { + if (this.Addon == nint.Zero) return InvalidAddon; + + var addonPointer = (AtkUnitBase*)this.Addon; + if (addonPointer->Name is null) return InvalidAddon; + + return this.addonName ??= MemoryHelper.ReadString((nint)addonPointer->Name, 0x20); + } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs new file mode 100644 index 000000000..d93001d1c --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -0,0 +1,10 @@ +namespace Dalamud.Game.Addon; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonDrawArgs : AddonArgs +{ + /// + public override AddonArgsType Type => AddonArgsType.Draw; +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs new file mode 100644 index 000000000..ed7aa1b3c --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -0,0 +1,10 @@ +namespace Dalamud.Game.Addon; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonFinalizeArgs : AddonArgs +{ + /// + public override AddonArgsType Type => AddonArgsType.Finalize; +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs new file mode 100644 index 000000000..60ccaf8ea --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -0,0 +1,28 @@ +using System; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonRefreshArgs : AddonArgs +{ + /// + public override AddonArgsType Type => AddonArgsType.Refresh; + + /// + /// Gets the number of AtkValues. + /// + public uint AtkValueCount { get; init; } + + /// + /// Gets the address of the AtkValue array. + /// + public nint AtkValues { get; init; } + + /// + /// Gets the AtkValues in the form of a span. + /// + public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs new file mode 100644 index 000000000..a31369aaf --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Game.Addon; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonRequestedUpdateArgs : AddonArgs +{ + /// + public override AddonArgsType Type => AddonArgsType.RequestedUpdate; + + /// + /// Gets the NumberArrayData** for this event. + /// + public nint NumberArrayData { get; init; } + + /// + /// Gets the StringArrayData** for this event. + /// + public nint StringArrayData { get; init; } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs new file mode 100644 index 000000000..17c87967a --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -0,0 +1,29 @@ +using System; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon; + +/// +/// Addon argument data for Setup events. +/// +public class AddonSetupArgs : AddonArgs +{ + /// + public override AddonArgsType Type => AddonArgsType.Setup; + + /// + /// Gets the number of AtkValues. + /// + public uint AtkValueCount { get; init; } + + /// + /// Gets the address of the AtkValue array. + /// + public nint AtkValues { get; init; } + + /// + /// Gets the AtkValues in the form of a span. + /// + public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs new file mode 100644 index 000000000..993883d77 --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -0,0 +1,15 @@ +namespace Dalamud.Game.Addon; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonUpdateArgs : AddonArgs +{ + /// + public override AddonArgsType Type => AddonArgsType.Update; + + /// + /// Gets the time since the last update. + /// + public float TimeDelta { get; init; } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgs.cs deleted file mode 100644 index 50c995abb..000000000 --- a/Dalamud/Game/AddonLifecycle/AddonArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Dalamud.Memory; -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace Dalamud.Game.AddonLifecycle; - -/// -/// Addon argument data for use in event subscribers. -/// -public unsafe class AddonArgs -{ - private string? addonName; - - /// - /// Gets the name of the addon this args referrers to. - /// - public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); - - /// - /// Gets the pointer to the addons AtkUnitBase. - /// - required public nint Addon { get; init; } -} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgsType.cs b/Dalamud/Game/AddonLifecycle/AddonArgsType.cs new file mode 100644 index 000000000..8a07d445b --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgsType.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Game.Addon; + +/// +/// Enumeration for available AddonLifecycle arg data. +/// +public enum AddonArgsType +{ + /// + /// Contains argument data for Setup. + /// + Setup, + + /// + /// Contains argument data for Update. + /// + Update, + + /// + /// Contains argument data for Draw. + /// + Draw, + + /// + /// Contains argument data for Finalize. + /// + Finalize, + + /// + /// Contains argument data for RequestedUpdate. + /// + RequestedUpdate, + + /// + /// Contains argument data for Refresh. + /// + Refresh, +} diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs index faef30c88..cfc83fb8a 100644 --- a/Dalamud/Game/AddonLifecycle/AddonEvent.cs +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonLifecycle; +namespace Dalamud.Game.Addon; /// /// Enumeration for available AddonLifecycle events. diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index a58e16e0c..8de98abcc 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -11,7 +11,7 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.AddonLifecycle; +namespace Dalamud.Game.Addon; /// /// This class provides events for in-game addon lifecycles. @@ -26,7 +26,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly Framework framework = Service.Get(); private readonly AddonLifecycleAddressResolver address; - private readonly Hook onAddonSetupHook; + private readonly CallHook onAddonSetupHook; private readonly Hook onAddonFinalizeHook; private readonly CallHook onAddonDrawHook; private readonly CallHook onAddonUpdateHook; @@ -45,7 +45,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.framework.Update += this.OnFrameworkUpdate; - this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); + this.onAddonSetupHook = new CallHook(this.address.AddonSetup, this.OnAddonSetup); this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); @@ -53,7 +53,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); } - private delegate nint AddonSetupDelegate(AtkUnitBase* addon); + private delegate void AddonSetupDelegate(AtkUnitBase* addon, uint valueCount, AtkValue* values); private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); @@ -136,36 +136,44 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - private nint OnAddonSetup(AtkUnitBase* addon) + private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { try { - this.InvokeListeners(AddonEvent.PreSetup, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs + { + Addon = (nint)addon, + AtkValueCount = valueCount, + AtkValues = (nint)values, + }); } catch (Exception e) { Log.Error(e, "Exception in OnAddonSetup pre-setup invoke."); } - var result = this.onAddonSetupHook.Original(addon); + addon->OnSetup(valueCount, values); try { - this.InvokeListeners(AddonEvent.PostSetup, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs + { + Addon = (nint)addon, + AtkValueCount = valueCount, + AtkValues = (nint)values, + }); } catch (Exception e) { Log.Error(e, "Exception in OnAddonSetup post-setup invoke."); } - - return result; } private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { try { - this.InvokeListeners(AddonEvent.PreFinalize, new AddonArgs { Addon = (nint)atkUnitBase[0] }); + this.InvokeListeners(AddonEvent.PreFinalize, new AddonFinalizeArgs { Addon = (nint)atkUnitBase[0] }); } catch (Exception e) { @@ -179,7 +187,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreDraw, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreDraw, new AddonDrawArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -190,7 +198,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostDraw, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostDraw, new AddonDrawArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -202,7 +210,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreUpdate, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); } catch (Exception e) { @@ -213,7 +221,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostUpdate, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); } catch (Exception e) { @@ -225,7 +233,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreRefresh, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreRefresh, new AddonRefreshArgs + { + Addon = (nint)addon, + AtkValueCount = valueCount, + AtkValues = (nint)values, + }); } catch (Exception e) { @@ -236,7 +249,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostRefresh, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostRefresh, new AddonRefreshArgs + { + Addon = (nint)addon, + AtkValueCount = valueCount, + AtkValues = (nint)values, + }); } catch (Exception e) { @@ -250,7 +268,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonRequestedUpdateArgs + { + Addon = (nint)addon, + NumberArrayData = (nint)numberArrayData, + StringArrayData = (nint)stringArrayData, + }); } catch (Exception e) { @@ -261,7 +284,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonRequestedUpdateArgs + { + Addon = (nint)addon, + NumberArrayData = (nint)numberArrayData, + StringArrayData = (nint)stringArrayData, + }); } catch (Exception e) { diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index 079e09c80..16fd54832 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonLifecycle; +namespace Dalamud.Game.Addon; /// /// AddonLifecycleService memory address resolver. @@ -41,7 +41,7 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver /// The signature scanner to facilitate setup. protected override void Setup64Bit(SigScanner sig) { - this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); + this.AddonSetup = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 93 ?? ?? ?? ?? 80 8B"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs index 0f088362d..12ccf5e8f 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin.Services; -namespace Dalamud.Game.AddonLifecycle; +namespace Dalamud.Game.Addon; /// /// This class is a helper for tracking and invoking listener delegates. diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 5467e207f..e8e6ca6c5 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -3,20 +3,16 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; -using Dalamud.Game.AddonEventManager; +using Dalamud.Game.Addon; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; -using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; -using DalamudAddonEventManager = Dalamud.Game.AddonEventManager.AddonEventManager; - namespace Dalamud.Game.Gui.Dtr; /// @@ -27,9 +23,6 @@ namespace Dalamud.Game.Gui.Dtr; internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; - private const uint MouseOverEventIdOffset = 10000; - private const uint MouseOutEventIdOffset = 20000; - private const uint MouseClickEventIdOffset = 30000; private static readonly ModuleLog Log = new("DtrBar"); @@ -43,23 +36,29 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private readonly DalamudConfiguration configuration = Service.Get(); [ServiceManager.ServiceDependency] - private readonly DalamudAddonEventManager uiEventManager = Service.Get(); + private readonly AddonEventManager uiEventManager = Service.Get(); - private readonly DtrBarAddressResolver address; + [ServiceManager.ServiceDependency] + private readonly AddonLifecycle addonLifecycle = Service.Get(); + + private readonly AddonLifecycleEventListener dtrPostDrawListener; + private readonly AddonLifecycleEventListener dtrPostRequestedUpdateListener; + private readonly ConcurrentBag newEntries = new(); private readonly List entries = new(); - private readonly Hook onAddonDrawHook; - private readonly Hook onAddonRequestedUpdateHook; + + private readonly Dictionary> eventHandles = new(); + private uint runningNodeIds = BaseNodeId; [ServiceManager.ServiceConstructor] - private DtrBar(SigScanner sigScanner) + private DtrBar() { - this.address = new DtrBarAddressResolver(); - this.address.Setup(sigScanner); + this.dtrPostDrawListener = new AddonLifecycleEventListener(AddonEvent.PostDraw, "_DTR", this.OnDtrPostDraw); + this.dtrPostRequestedUpdateListener = new AddonLifecycleEventListener(AddonEvent.PostRequestedUpdate, "_DTR", this.OnAddonRequestedUpdateDetour); - this.onAddonDrawHook = Hook.FromAddress(this.address.AtkUnitBaseDraw, this.OnAddonDrawDetour); - this.onAddonRequestedUpdateHook = Hook.FromAddress(this.address.AddonRequestedUpdate, this.OnAddonRequestedUpdateDetour); + this.addonLifecycle.RegisterListener(this.dtrPostDrawListener); + this.addonLifecycle.RegisterListener(this.dtrPostRequestedUpdateListener); this.framework.Update += this.Update; @@ -68,10 +67,6 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.configuration.QueueSave(); } - private delegate void AddonDrawDelegate(AtkUnitBase* addon); - - private delegate void AddonRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); - /// public DtrBarEntry Get(string title, SeString? text = null) { @@ -102,8 +97,8 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar /// void IDisposable.Dispose() { - this.onAddonDrawHook.Dispose(); - this.onAddonRequestedUpdateHook.Dispose(); + this.addonLifecycle.UnregisterListener(this.dtrPostDrawListener); + this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener); foreach (var entry in this.entries) this.RemoveNode(entry.TextNode); @@ -165,13 +160,6 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar return xPos.CompareTo(yPos); }); } - - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.onAddonDrawHook.Enable(); - this.onAddonRequestedUpdateHook.Enable(); - } private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); @@ -181,7 +169,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.HandleAddedNodes(); var dtr = this.GetDtr(); - if (dtr == null) return; + if (dtr == null || dtr->RootNode == null || dtr->RootNode->ChildNode == null) return; // The collision node on the DTR element is always the width of its content if (dtr->UldManager.NodeList == null) return; @@ -232,7 +220,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (this.configuration.DtrSwapDirection) { - data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); + data.TextNode->AtkResNode.SetPositionFloat(runningXPos + this.configuration.DtrSpacing, 2); runningXPos += elementWidth; } else @@ -241,6 +229,11 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); } } + else + { + // If we want the node hidden, shift it up, to prevent collision conflicts + data.TextNode->AtkResNode.SetY(-collisionNode->Height * dtr->RootNode->ScaleX); + } } } @@ -258,37 +251,26 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.ApplySort(); } } - - // This hooks all AtkUnitBase.Draw calls, then checks for our specific addon name. - // AddonDtr doesn't implement it's own Draw method, would need to replace vtable entry to be more efficient. - private void OnAddonDrawDetour(AtkUnitBase* addon) + + private void OnDtrPostDraw(AddonEvent eventType, AddonArgs addonInfo) { - this.onAddonDrawHook!.Original(addon); + var addon = (AtkUnitBase*)addonInfo.Addon; - try - { - if (MemoryHelper.ReadString((nint)addon->Name, 0x20) is not "_DTR") return; - - this.UpdateNodePositions(addon); + this.UpdateNodePositions(addon); - if (!this.configuration.DtrSwapDirection) - { - var targetSize = (ushort)this.CalculateTotalSize(); - var sizeDelta = targetSize - addon->RootNode->Width; - - if (addon->RootNode->Width != targetSize) - { - addon->RootNode->SetWidth(targetSize); - addon->SetX((short)(addon->GetX() - sizeDelta)); - - // force a RequestedUpdate immediately to force the game to right-justify it immediately. - addon->OnUpdate(AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); - } - } - } - catch (Exception e) + if (!this.configuration.DtrSwapDirection) { - Log.Error(e, "Exception in OnAddonDraw."); + var targetSize = (ushort)this.CalculateTotalSize(); + var sizeDelta = MathF.Round((targetSize - addon->RootNode->Width) * addon->RootNode->ScaleX); + + if (addon->RootNode->Width != targetSize) + { + addon->RootNode->SetWidth(targetSize); + addon->SetX((short)(addon->GetX() - sizeDelta)); + + // force a RequestedUpdate immediately to force the game to right-justify it immediately. + addon->OnUpdate(AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); + } } } @@ -315,18 +297,11 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } - private void OnAddonRequestedUpdateDetour(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + private void OnAddonRequestedUpdateDetour(AddonEvent eventType, AddonArgs addonInfo) { - this.onAddonRequestedUpdateHook.Original(addon, numberArrayData, stringArrayData); - - try - { - this.UpdateNodePositions(addon); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRequestedUpdate."); - } + var addon = (AtkUnitBase*)addonInfo.Addon; + + this.UpdateNodePositions(addon); } /// @@ -350,6 +325,11 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private void RecreateNodes() { this.runningNodeIds = BaseNodeId; + if (this.entries.Any()) + { + this.eventHandles.Clear(); + } + foreach (var entry in this.entries) { entry.TextNode = this.MakeNode(++this.runningNodeIds); @@ -384,10 +364,14 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; - this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); - this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); - this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); - + this.eventHandles.TryAdd(node->AtkResNode.NodeID, new List()); + this.eventHandles[node->AtkResNode.NodeID].AddRange(new List + { + this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler), + this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler), + this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler), + }); + var lastChild = dtr->RootNode->ChildNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; Log.Debug($"Found last sibling: {(ulong)lastChild:X}"); @@ -404,14 +388,13 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar return true; } - private bool RemoveNode(AtkTextNode* node) + private void RemoveNode(AtkTextNode* node) { var dtr = this.GetDtr(); - if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; + if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)node, AddonEventType.MouseOver); - this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)node, AddonEventType.MouseOut); - this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)node, AddonEventType.MouseClick); + this.eventHandles[node->AtkResNode.NodeID].ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); + this.eventHandles[node->AtkResNode.NodeID].Clear(); var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; @@ -427,7 +410,6 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar dtr->UldManager.UpdateDrawNodeList(); dtr->UpdateCollisionNodeList(false); Log.Debug("Updated node draw list"); - return true; } private AtkTextNode* MakeNode(uint nodeId) diff --git a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs deleted file mode 100644 index 744d926f0..000000000 --- a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Dalamud.Game.Gui.Dtr; - -/// -/// DtrBar memory address resolver. -/// -internal class DtrBarAddressResolver : BaseAddressResolver -{ - /// - /// Gets the address of the AtkUnitBaseDraw method. - /// This is the base handler for all addons. - /// We will use this here because _DTR does not have a overloaded handler, so we must use the base handler. - /// - public nint AtkUnitBaseDraw { get; private set; } - - /// - /// Gets the address of the DTRRequestUpdate method. - /// - public nint AddonRequestedUpdate { get; private set; } - - /// - /// Scan for and setup any configured address pointers. - /// - /// The signature scanner to facilitate setup. - protected override void Setup64Bit(SigScanner scanner) - { - this.AtkUnitBaseDraw = scanner.ScanText("48 83 EC 28 F6 81 ?? ?? ?? ?? ?? 4C 8B C1"); - this.AddonRequestedUpdate = scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B BA ?? ?? ?? ?? 48 8B F1 49 8B 98 ?? ?? ?? ?? 33 D2"); - } -} diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs index ad9eedaa9..dd9ab08f7 100644 --- a/Dalamud/Interface/ColorHelpers.cs +++ b/Dalamud/Interface/ColorHelpers.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; using System.Numerics; namespace Dalamud.Interface; @@ -267,4 +268,32 @@ public static class ColorHelpers /// The faded color. public static uint Fade(uint color, float amount) => RgbaVector4ToUint(Fade(RgbaUintToVector4(color), amount)); + + /// + /// Convert a KnownColor to a RGBA vector with values between 0.0f and 1.0f + /// + /// Known Color to convert. + /// RGBA Vector with values between 0.0f and 1.0f. + public static Vector4 Vector(this KnownColor knownColor) + { + var rgbColor = Color.FromKnownColor(knownColor); + return new Vector4(rgbColor.R, rgbColor.G, rgbColor.B, rgbColor.A) / 255.0f; + } + + /// + /// Normalizes a Vector4 with RGBA 255 color values to values between 0.0f and 1.0f + /// If values are out of RGBA 255 range, the original value is returned. + /// + /// The color vector to convert. + /// A vector with values between 0.0f and 1.0f. + public static Vector4 NormalizeToUnitRange(this Vector4 color) => color switch + { + // If any components are out of range, return original value. + { W: > 255.0f or < 0.0f } or { X: > 255.0f or < 0.0f } or { Y: > 255.0f or < 0.0f } or { Z: > 255.0f or < 0.0f } => color, + + // If all components are already unit range, return original value. + { W: >= 0.0f and <= 1.0f, X: >= 0.0f and <= 1.0f, Y: >= 0.0f and <= 1.0f, Z: >= 0.0f and <= 1.0f } => color, + + _ => color / 255.0f, + }; } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs index 3a1cb0e77..a9948430f 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using Dalamud.Game.AddonLifecycle; +using Dalamud.Game.Addon; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index a339b807d..ad424f59a 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -3,6 +3,7 @@ using System.Numerics; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; @@ -13,6 +14,8 @@ namespace Dalamud.Interface.Windowing; /// public abstract class Window { + private static readonly ModuleLog Log = new("WindowSystem"); + private static bool wasEscPressedLastFrame = false; private bool internalLastIsOpen = false; @@ -286,7 +289,14 @@ public abstract class Window if (this.ShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, this.Flags) : ImGui.Begin(this.WindowName, this.Flags)) { // Draw the actual window contents - this.Draw(); + try + { + this.Draw(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error during Draw(): {this.WindowName}"); + } } if (wasFocused) diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index dbbfd784b..52f836b4f 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.AddonEventManager; +using Dalamud.Game.Addon; namespace Dalamud.Plugin.Services; @@ -18,20 +18,18 @@ public interface IAddonEventManager /// /// Registers an event handler for the specified addon, node, and type. /// - /// Unique Id for this event, maximum 0x10000. /// The parent addon for this event. /// The node that will trigger this event. /// The event type for this event. /// The handler to call when event is triggered. - void AddEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler); + /// IAddonEventHandle used to remove the event. Null if no event was added. + IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler); /// /// Unregisters an event handler with the specified event id and event type. /// - /// The Unique Id for this event. - /// The node for this event. - /// The event type for this event. - void RemoveEvent(uint eventId, nint atkResNode, AddonEventType eventType); + /// Unique handle identifying this event. + void RemoveEvent(IAddonEventHandle eventHandle); /// /// Force the game cursor to be the specified cursor. diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 1dc792660..2bc41a366 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; -using Dalamud.Game.AddonLifecycle; +using Dalamud.Game.Addon; namespace Dalamud.Plugin.Services; @@ -13,9 +13,9 @@ public interface IAddonLifecycle /// /// Delegate for receiving addon lifecycle event messages. /// - /// The event type that triggered the message. - /// Information about what addon triggered the message. - public delegate void AddonEventDelegate(AddonEvent eventType, AddonArgs addonInfo); + /// The event type that triggered the message. + /// Information about what addon triggered the message. + public delegate void AddonEventDelegate(AddonEvent type, AddonArgs args); /// /// Register a listener that will trigger on the specified event and any of the specified addons. diff --git a/targets/Dalamud.Plugin.Bootstrap.targets b/targets/Dalamud.Plugin.Bootstrap.targets index c30a5acba..db4bf6cd7 100644 --- a/targets/Dalamud.Plugin.Bootstrap.targets +++ b/targets/Dalamud.Plugin.Bootstrap.targets @@ -1,11 +1,10 @@ - $(appdata)\XIVLauncher\addon\Hooks\dev\ - - - - $(DALAMUD_HOME)/ + $(appdata)\XIVLauncher\addon\Hooks\dev\ + $(HOME)/.xlcore/dalamud/Hooks/dev/ + $(HOME)/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/ + $(DALAMUD_HOME)/