Merge pull request #1408 from MidoriKami/AddonEventManager_Enhancements

This commit is contained in:
goat 2023-09-21 21:30:41 +02:00 committed by GitHub
commit dc54f040b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 443 additions and 243 deletions

View file

@ -1,4 +1,4 @@
namespace Dalamud.Game.AddonEventManager;
namespace Dalamud.Game.Addon;
/// <summary>
/// Reimplementation of CursorType.

View file

@ -0,0 +1,54 @@
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon;
/// <summary>
/// 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.
/// </summary>
internal unsafe class AddonEventEntry
{
/// <summary>
/// Name of an invalid addon.
/// </summary>
public const string InvalidAddonName = "NullAddon";
private string? addonName;
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
/// </summary>
required public nint Addon { get; init; }
/// <summary>
/// Gets the name of the addon this args referrers to.
/// </summary>
public string AddonName => this.Addon == nint.Zero ? InvalidAddonName : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20);
/// <summary>
/// Gets the pointer to the event source.
/// </summary>
required public nint Node { get; init; }
/// <summary>
/// Gets the handler that gets called when this event is triggered.
/// </summary>
required public IAddonEventManager.AddonEventHandler Handler { get; init; }
/// <summary>
/// Gets the unique id for this event.
/// </summary>
required public uint ParamKey { get; init; }
/// <summary>
/// Gets the event type for this event.
/// </summary>
required public AddonEventType EventType { get; init; }
/// <summary>
/// Gets the formatted log string for this AddonEventEntry.
/// </summary>
internal string LogString => $"ParamKey: {this.ParamKey}, Addon: {this.AddonName}, Event: {this.EventType}";
}

View file

@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.AddonEventManager;
namespace Dalamud.Game.Addon;
/// <summary>
/// Event listener class for managing custom events.
@ -40,6 +40,11 @@ internal unsafe class AddonEventListener : IDisposable
/// <param name="unknown">Unknown Parameter.</param>
public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown);
/// <summary>
/// Gets the address of this listener.
/// </summary>
public nint Address => (nint)this.eventListener;
/// <inheritdoc />
public void Dispose()
{

View file

@ -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;
/// <summary>
/// Service provider for addon event management.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEventManager
internal unsafe class AddonEventManager : IDisposable, IServiceType
{
/// <summary>
/// PluginName for Dalamud Internal use.
/// </summary>
public const string DalamudInternalKey = "Dalamud.Internal";
private static readonly ModuleLog Log = new("AddonEventManager");
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
private readonly AddonLifecycleEventListener finalizeEventListener;
private readonly AddonEventManagerAddressResolver address;
private readonly Hook<UpdateCursorDelegate> onUpdateCursor;
private readonly AddonEventListener eventListener;
private readonly Dictionary<uint, IAddonEventManager.AddonEventHandler> eventHandlers;
private readonly List<PluginEventController> pluginEventControllers;
private AddonCursorType? cursorOverride;
@ -34,64 +45,109 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent
this.address = new AddonEventManagerAddressResolver();
this.address.Setup(sigScanner);
this.eventHandlers = new Dictionary<uint, IAddonEventManager.AddonEventHandler>();
this.eventListener = new AddonEventListener(this.DalamudAddonEventHandler);
this.pluginEventControllers = new List<PluginEventController>
{
new(DalamudInternalKey), // Create entry for Dalamud's Internal Use.
};
this.cursorOverride = null;
this.onUpdateCursor = Hook<UpdateCursorDelegate>.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);
/// <inheritdoc/>
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}");
}
}
/// <inheritdoc/>
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}");
}
}
/// <inheritdoc/>
public void Dispose()
{
this.onUpdateCursor.Dispose();
this.eventListener.Dispose();
this.eventHandlers.Clear();
foreach (var pluginEventController in this.pluginEventControllers)
{
pluginEventController.Dispose();
}
/// <inheritdoc/>
public void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
this.addonLifecycle.UnregisterListener(this.finalizeEventListener);
}
/// <inheritdoc/>
public void ResetCursor() => this.cursorOverride = null;
/// <summary>
/// Registers an event handler for the specified addon, node, and type.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
/// <param name="eventId">Unique Id for this event, maximum 0x10000.</param>
/// <param name="atkUnitBase">The parent addon for this event.</param>
/// <param name="atkResNode">The node that will trigger this event.</param>
/// <param name="eventType">The event type for this event.</param>
/// <param name="eventHandler">The handler to call when event is triggered.</param>
internal void AddEvent(string pluginId, uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
{
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController)
{
eventController.AddEvent(eventId, atkUnitBase, atkResNode, eventType, eventHandler);
}
else
{
Log.Verbose($"Unable to locate controller for {pluginId}. No event was added.");
}
}
/// <summary>
/// Unregisters an event handler with the specified event id and event type.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
/// <param name="eventId">The Unique Id for this event.</param>
internal void RemoveEvent(string pluginId, uint eventId)
{
if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController)
{
eventController.RemoveEvent(eventId);
}
else
{
Log.Verbose($"Unable to locate controller for {pluginId}. No event was removed.");
}
}
/// <summary>
/// Force the game cursor to be the specified cursor.
/// </summary>
/// <param name="cursor">Which cursor to use.</param>
internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
/// <summary>
/// Un-forces the game cursor.
/// </summary>
internal void ResetCursor() => this.cursorOverride = null;
/// <summary>
/// Adds a new managed event controller if one doesn't already exist for this pluginId.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
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));
}
}
/// <summary>
/// Removes an existing managed event controller for the specified plugin.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
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 +155,22 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent
this.onUpdateCursor.Enable();
}
/// <summary>
/// When an addon finalizes, check it for any registered events, and unregister them.
/// </summary>
/// <param name="eventType">Event type that triggered this call.</param>
/// <param name="addonInfo">Addon that triggered this call.</param>
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 +195,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.");
}
}
}
}
/// <summary>
@ -150,25 +206,24 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent
#pragma warning disable SA1015
[ResolveVia<IAddonEventManager>]
#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<AddonEventManager>.Get();
private readonly AddonEventManager eventManagerService = Service<AddonEventManager>.Get();
private readonly AddonEventListener eventListener;
private readonly Dictionary<uint, IAddonEventManager.AddonEventHandler> eventHandlers;
private readonly LocalPlugin plugin;
private bool isForcingCursor;
/// <summary>
/// Initializes a new instance of the <see cref="AddonEventManagerPluginScoped"/> class.
/// </summary>
public AddonEventManagerPluginScoped()
/// <param name="plugin">Plugin info for the plugin that requested this service.</param>
public AddonEventManagerPluginScoped(LocalPlugin plugin)
{
this.eventHandlers = new Dictionary<uint, IAddonEventManager.AddonEventHandler>();
this.eventListener = new AddonEventListener(this.PluginAddonEventHandler);
this.plugin = plugin;
this.eventManagerService.AddPluginEventController(plugin.Manifest.WorkingPluginId.ToString());
}
/// <inheritdoc/>
@ -177,54 +232,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());
}
/// <inheritdoc/>
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}");
}
}
=> this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventId, atkUnitBase, atkResNode, eventType, eventHandler);
/// <inheritdoc/>
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(uint eventId)
=> this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventId);
/// <inheritdoc/>
public void SetCursor(AddonCursorType cursor)
{
this.isForcingCursor = true;
this.baseEventManager.SetCursor(cursor);
this.eventManagerService.SetCursor(cursor);
}
/// <inheritdoc/>
@ -232,22 +259,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();
}
}

View file

@ -1,4 +1,4 @@
namespace Dalamud.Game.AddonEventManager;
namespace Dalamud.Game.Addon;
/// <summary>
/// AddonEventManager memory address resolver.

View file

@ -1,4 +1,4 @@
namespace Dalamud.Game.AddonEventManager;
namespace Dalamud.Game.Addon;
/// <summary>
/// Reimplementation of AtkEventType.

View file

@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Gui;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon;
/// <summary>
/// Class to manage creating and cleaning up events per-plugin.
/// </summary>
internal unsafe class PluginEventController : IDisposable
{
private static readonly ModuleLog Log = new("AddonEventManager");
/// <summary>
/// Initializes a new instance of the <see cref="PluginEventController"/> class.
/// </summary>
/// <param name="pluginId">The Unique ID for this plugin.</param>
public PluginEventController(string pluginId)
{
this.PluginId = pluginId;
this.EventListener = new AddonEventListener(this.PluginEventListHandler);
}
/// <summary>
/// Gets the unique ID for this PluginEventList.
/// </summary>
public string PluginId { get; init; }
private AddonEventListener EventListener { get; init; }
private List<AddonEventEntry> Events { get; } = new();
/// <summary>
/// Adds a tracked event.
/// </summary>
/// <param name="eventId">Unique ID of the event to add.</param>
/// <param name="atkUnitBase">The Parent addon for the event.</param>
/// <param name="atkResNode">The Node for the event.</param>
/// <param name="atkEventType">The Event Type.</param>
/// <param name="handler">The delegate to call when invoking this event.</param>
public void AddEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler)
{
var node = (AtkResNode*)atkResNode;
var addon = (AtkUnitBase*)atkUnitBase;
var eventType = (AtkEventType)atkEventType;
var eventEntry = new AddonEventEntry
{
Addon = atkUnitBase,
Handler = handler,
Node = atkResNode,
EventType = atkEventType,
ParamKey = eventId,
};
Log.Verbose($"Adding Event: {eventEntry.LogString}");
this.EventListener.RegisterEvent(addon, node, eventType, eventId);
this.Events.Add(eventEntry);
}
/// <summary>
/// Removes a tracked event, also attempts to un-attach the event from native.
/// </summary>
/// <param name="eventId">Unique ID of the event to remove.</param>
public void RemoveEvent(uint eventId)
{
if (this.Events.FirstOrDefault(registeredEvent => registeredEvent.ParamKey == eventId) is not { } targetEvent) return;
Log.Verbose($"Removing Event: {targetEvent.LogString}");
this.TryRemoveEventFromNative(targetEvent);
this.Events.Remove(targetEvent);
}
/// <summary>
/// Removes all events attached to the specified addon.
/// </summary>
/// <param name="addonName">Addon name to remove events from.</param>
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.ParamKey);
}
}
}
/// <inheritdoc/>
public void Dispose()
{
foreach (var registeredEvent in this.Events.ToList())
{
this.RemoveEvent(registeredEvent.ParamKey);
}
this.EventListener.Dispose();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="eventEntry">Event entry to remove.</param>
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<GameGui>.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.");
}
}
}

View file

@ -1,7 +1,7 @@
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.AddonLifecycle;
namespace Dalamud.Game.Addon;
/// <summary>
/// Addon argument data for use in event subscribers.

View file

@ -1,4 +1,4 @@
namespace Dalamud.Game.AddonLifecycle;
namespace Dalamud.Game.Addon;
/// <summary>
/// Enumeration for available AddonLifecycle events.

View file

@ -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;
/// <summary>
/// This class provides events for in-game addon lifecycles.

View file

@ -1,4 +1,4 @@
namespace Dalamud.Game.AddonLifecycle;
namespace Dalamud.Game.Addon;
/// <summary>
/// AddonLifecycleService memory address resolver.

View file

@ -1,6 +1,6 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.AddonLifecycle;
namespace Dalamud.Game.Addon;
/// <summary>
/// This class is a helper for tracking and invoking listener delegates.

View file

@ -4,20 +4,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;
/// <summary>
@ -45,23 +41,27 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudAddonEventManager uiEventManager = Service<DalamudAddonEventManager>.Get();
private readonly AddonEventManager uiEventManager = Service<AddonEventManager>.Get();
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
private readonly AddonLifecycleEventListener dtrPostDrawListener;
private readonly AddonLifecycleEventListener dtrPostRequestedUpdateListener;
private readonly DtrBarAddressResolver address;
private readonly ConcurrentBag<DtrBarEntry> newEntries = new();
private readonly List<DtrBarEntry> entries = new();
private readonly Hook<AddonDrawDelegate> onAddonDrawHook;
private readonly Hook<AddonRequestedUpdateDelegate> onAddonRequestedUpdateHook;
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<AddonDrawDelegate>.FromAddress(this.address.AtkUnitBaseDraw, this.OnAddonDrawDetour);
this.onAddonRequestedUpdateHook = Hook<AddonRequestedUpdateDelegate>.FromAddress(this.address.AddonRequestedUpdate, this.OnAddonRequestedUpdateDetour);
this.addonLifecycle.RegisterListener(this.dtrPostDrawListener);
this.addonLifecycle.RegisterListener(this.dtrPostRequestedUpdateListener);
this.framework.Update += this.Update;
@ -70,10 +70,6 @@ public 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);
/// <inheritdoc/>
public DtrBarEntry Get(string title, SeString? text = null)
{
@ -104,8 +100,8 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
/// <inheritdoc/>
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);
@ -168,13 +164,6 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
});
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.onAddonDrawHook.Enable();
this.onAddonRequestedUpdateHook.Enable();
}
private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer();
private void Update(Framework unused)
@ -234,7 +223,7 @@ public 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
@ -243,6 +232,11 @@ public 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);
}
}
}
@ -261,22 +255,16 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
}
}
// 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);
try
{
if (MemoryHelper.ReadString((nint)addon->Name, 0x20) is not "_DTR") return;
var addon = (AtkUnitBase*)addonInfo.Addon;
this.UpdateNodePositions(addon);
if (!this.configuration.DtrSwapDirection)
{
var targetSize = (ushort)this.CalculateTotalSize();
var sizeDelta = targetSize - addon->RootNode->Width;
var sizeDelta = MathF.Round((targetSize - addon->RootNode->Width) * addon->RootNode->ScaleX);
if (addon->RootNode->Width != targetSize)
{
@ -288,11 +276,6 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
}
}
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonDraw.");
}
}
private void UpdateNodePositions(AtkUnitBase* addon)
{
@ -317,19 +300,12 @@ public 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);
var addon = (AtkUnitBase*)addonInfo.Addon;
try
{
this.UpdateNodePositions(addon);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonRequestedUpdate.");
}
}
/// <summary>
/// Checks if there are any Dalamud nodes in the DTR.
@ -386,9 +362,9 @@ public 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.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler);
this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler);
this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler);
var lastChild = dtr->RootNode->ChildNode;
while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
@ -406,14 +382,14 @@ public 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.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset);
this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset);
this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset);
var tmpPrevNode = node->AtkResNode.PrevSiblingNode;
var tmpNextNode = node->AtkResNode.NextSiblingNode;
@ -429,7 +405,6 @@ public 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)

View file

@ -1,29 +0,0 @@
namespace Dalamud.Game.Gui.Dtr;
/// <summary>
/// DtrBar memory address resolver.
/// </summary>
public class DtrBarAddressResolver : BaseAddressResolver
{
/// <summary>
/// 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.
/// </summary>
public nint AtkUnitBaseDraw { get; private set; }
/// <summary>
/// Gets the address of the DTRRequestUpdate method.
/// </summary>
public nint AddonRequestedUpdate { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="scanner">The signature scanner to facilitate setup.</param>
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");
}
}

View file

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

View file

@ -1,4 +1,4 @@
using Dalamud.Game.AddonEventManager;
using Dalamud.Game.Addon;
namespace Dalamud.Plugin.Services;
@ -29,9 +29,7 @@ public interface IAddonEventManager
/// Unregisters an event handler with the specified event id and event type.
/// </summary>
/// <param name="eventId">The Unique Id for this event.</param>
/// <param name="atkResNode">The node for this event.</param>
/// <param name="eventType">The event type for this event.</param>
void RemoveEvent(uint eventId, nint atkResNode, AddonEventType eventType);
void RemoveEvent(uint eventId);
/// <summary>
/// Force the game cursor to be the specified cursor.

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Game.AddonLifecycle;
using Dalamud.Game.Addon;
namespace Dalamud.Plugin.Services;