mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Merge remote-tracking branch 'origin/master' into v9-rollup
This commit is contained in:
commit
33868087ed
9 changed files with 687 additions and 78 deletions
22
Dalamud/Game/AddonLifecycle/AddonArgs.cs
Normal file
22
Dalamud/Game/AddonLifecycle/AddonArgs.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Dalamud.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.AddonLifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for use in event subscribers.
|
||||
/// </summary>
|
||||
public unsafe class AddonArgs
|
||||
{
|
||||
private string? addonName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon this args referrers to.
|
||||
/// </summary>
|
||||
public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pointer to the addons AtkUnitBase.
|
||||
/// </summary>
|
||||
required public nint Addon { get; init; }
|
||||
}
|
||||
62
Dalamud/Game/AddonLifecycle/AddonEvent.cs
Normal file
62
Dalamud/Game/AddonLifecycle/AddonEvent.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
namespace Dalamud.Game.AddonLifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// Enumeration for available AddonLifecycle events.
|
||||
/// </summary>
|
||||
public enum AddonEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Event that is fired before an addon begins it's setup process.
|
||||
/// </summary>
|
||||
PreSetup,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired after an addon has completed it's setup process.
|
||||
/// </summary>
|
||||
PostSetup,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired before an addon begins update.
|
||||
/// </summary>
|
||||
PreUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired after an addon has completed update.
|
||||
/// </summary>
|
||||
PostUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired before an addon begins draw.
|
||||
/// </summary>
|
||||
PreDraw,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired after an addon has completed draw.
|
||||
/// </summary>
|
||||
PostDraw,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired before an addon is finalized.
|
||||
/// </summary>
|
||||
PreFinalize,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired before an addon begins a requested update.
|
||||
/// </summary>
|
||||
PreRequestedUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired after an addon finishes a requested update.
|
||||
/// </summary>
|
||||
PostRequestedUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired before an addon begins a refresh.
|
||||
/// </summary>
|
||||
PreRefresh,
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired after an addon has finished a refresh.
|
||||
/// </summary>
|
||||
PostRefresh,
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Hooking.Internal;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
|
@ -14,12 +18,24 @@ namespace Dalamud.Game.AddonLifecycle;
|
|||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycle
|
||||
internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||
{
|
||||
private static readonly ModuleLog Log = new("AddonLifecycle");
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
private readonly AddonLifecycleAddressResolver address;
|
||||
private readonly Hook<AddonSetupDelegate> onAddonSetupHook;
|
||||
private readonly Hook<AddonFinalizeDelegate> onAddonFinalizeHook;
|
||||
private readonly CallHook<AddonDrawDelegate> onAddonDrawHook;
|
||||
private readonly CallHook<AddonUpdateDelegate> onAddonUpdateHook;
|
||||
private readonly Hook<AddonOnRefreshDelegate> onAddonRefreshHook;
|
||||
private readonly CallHook<AddonOnRequestedUpdateDelegate> onAddonRequestedUpdateHook;
|
||||
|
||||
private readonly ConcurrentBag<AddonLifecycleEventListener> newEventListeners = new();
|
||||
private readonly ConcurrentBag<AddonLifecycleEventListener> removeEventListeners = new();
|
||||
private readonly List<AddonLifecycleEventListener> eventListeners = new();
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonLifecycle(SigScanner sigScanner)
|
||||
|
|
@ -27,28 +43,77 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl
|
|||
this.address = new AddonLifecycleAddressResolver();
|
||||
this.address.Setup(sigScanner);
|
||||
|
||||
this.framework.Update += this.OnFrameworkUpdate;
|
||||
|
||||
this.onAddonSetupHook = Hook<AddonSetupDelegate>.FromAddress(this.address.AddonSetup, this.OnAddonSetup);
|
||||
this.onAddonFinalizeHook = Hook<AddonFinalizeDelegate>.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
|
||||
this.onAddonDrawHook = new CallHook<AddonDrawDelegate>(this.address.AddonDraw, this.OnAddonDraw);
|
||||
this.onAddonUpdateHook = new CallHook<AddonUpdateDelegate>(this.address.AddonUpdate, this.OnAddonUpdate);
|
||||
this.onAddonRefreshHook = Hook<AddonOnRefreshDelegate>.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh);
|
||||
this.onAddonRequestedUpdateHook = new CallHook<AddonOnRequestedUpdateDelegate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
|
||||
}
|
||||
|
||||
private delegate nint AddonSetupDelegate(AtkUnitBase* addon);
|
||||
|
||||
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IAddonLifecycle.AddonArgs>? AddonPreSetup;
|
||||
private delegate void AddonDrawDelegate(AtkUnitBase* addon);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IAddonLifecycle.AddonArgs>? AddonPostSetup;
|
||||
private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IAddonLifecycle.AddonArgs>? AddonPreFinalize;
|
||||
private delegate void AddonOnRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData);
|
||||
|
||||
private delegate void AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.framework.Update -= this.OnFrameworkUpdate;
|
||||
|
||||
this.onAddonSetupHook.Dispose();
|
||||
this.onAddonFinalizeHook.Dispose();
|
||||
this.onAddonDrawHook.Dispose();
|
||||
this.onAddonUpdateHook.Dispose();
|
||||
this.onAddonRefreshHook.Dispose();
|
||||
this.onAddonRequestedUpdateHook.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a listener for the target event and addon.
|
||||
/// </summary>
|
||||
/// <param name="listener">The listener to register.</param>
|
||||
internal void RegisterListener(AddonLifecycleEventListener listener)
|
||||
{
|
||||
this.newEventListeners.Add(listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the listener from events.
|
||||
/// </summary>
|
||||
/// <param name="listener">The listener to unregister.</param>
|
||||
internal void UnregisterListener(AddonLifecycleEventListener listener)
|
||||
{
|
||||
this.removeEventListeners.Add(listener);
|
||||
}
|
||||
|
||||
// Used to prevent concurrency issues if plugins try to register during iteration of listeners.
|
||||
private void OnFrameworkUpdate(Framework unused)
|
||||
{
|
||||
if (this.newEventListeners.Any())
|
||||
{
|
||||
this.eventListeners.AddRange(this.newEventListeners);
|
||||
this.newEventListeners.Clear();
|
||||
}
|
||||
|
||||
if (this.removeEventListeners.Any())
|
||||
{
|
||||
foreach (var toRemoveListener in this.removeEventListeners)
|
||||
{
|
||||
this.eventListeners.Remove(toRemoveListener);
|
||||
}
|
||||
|
||||
this.removeEventListeners.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
|
|
@ -56,16 +121,26 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl
|
|||
{
|
||||
this.onAddonSetupHook.Enable();
|
||||
this.onAddonFinalizeHook.Enable();
|
||||
this.onAddonDrawHook.Enable();
|
||||
this.onAddonUpdateHook.Enable();
|
||||
this.onAddonRefreshHook.Enable();
|
||||
this.onAddonRequestedUpdateHook.Enable();
|
||||
}
|
||||
|
||||
private void InvokeListeners(AddonEvent eventType, AddonArgs args)
|
||||
{
|
||||
// Match on string.empty for listeners that want events for all addons.
|
||||
foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty)))
|
||||
{
|
||||
listener.FunctionDelegate.Invoke(eventType, args);
|
||||
}
|
||||
}
|
||||
|
||||
private nint OnAddonSetup(AtkUnitBase* addon)
|
||||
{
|
||||
if (addon is null)
|
||||
return this.onAddonSetupHook.Original(addon);
|
||||
|
||||
try
|
||||
{
|
||||
this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon });
|
||||
this.InvokeListeners(AddonEvent.PreSetup, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -76,7 +151,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl
|
|||
|
||||
try
|
||||
{
|
||||
this.AddonPostSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon });
|
||||
this.InvokeListeners(AddonEvent.PostSetup, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -88,15 +163,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl
|
|||
|
||||
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
|
||||
{
|
||||
if (atkUnitBase is null || atkUnitBase[0] is null)
|
||||
{
|
||||
this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] });
|
||||
this.InvokeListeners(AddonEvent.PreFinalize, new AddonArgs { Addon = (nint)atkUnitBase[0] });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -105,6 +174,98 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl
|
|||
|
||||
this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
|
||||
}
|
||||
|
||||
private void OnAddonDraw(AtkUnitBase* addon)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.InvokeListeners(AddonEvent.PreDraw, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonDraw pre-draw invoke.");
|
||||
}
|
||||
|
||||
addon->Draw();
|
||||
|
||||
try
|
||||
{
|
||||
this.InvokeListeners(AddonEvent.PostDraw, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonDraw post-draw invoke.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.InvokeListeners(AddonEvent.PreUpdate, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonUpdate pre-update invoke.");
|
||||
}
|
||||
|
||||
addon->Update(delta);
|
||||
|
||||
try
|
||||
{
|
||||
this.InvokeListeners(AddonEvent.PostUpdate, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonUpdate post-update invoke.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.InvokeListeners(AddonEvent.PreRefresh, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke.");
|
||||
}
|
||||
|
||||
this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values);
|
||||
|
||||
try
|
||||
{
|
||||
this.InvokeListeners(AddonEvent.PostRefresh, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke.");
|
||||
}
|
||||
|
||||
addon->OnUpdate(numberArrayData, stringArrayData);
|
||||
|
||||
try
|
||||
{
|
||||
this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonArgs { Addon = (nint)addon });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -121,36 +282,81 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonLifecyclePluginScoped"/> class.
|
||||
/// </summary>
|
||||
public AddonLifecyclePluginScoped()
|
||||
{
|
||||
this.addonLifecycleService.AddonPreSetup += this.AddonPreSetupForward;
|
||||
this.addonLifecycleService.AddonPostSetup += this.AddonPostSetupForward;
|
||||
this.addonLifecycleService.AddonPreFinalize += this.AddonPreFinalizeForward;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IAddonLifecycle.AddonArgs>? AddonPreSetup;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IAddonLifecycle.AddonArgs>? AddonPostSetup;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<IAddonLifecycle.AddonArgs>? AddonPreFinalize;
|
||||
private readonly List<AddonLifecycleEventListener> eventListeners = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.addonLifecycleService.AddonPreSetup -= this.AddonPreSetupForward;
|
||||
this.addonLifecycleService.AddonPostSetup -= this.AddonPostSetupForward;
|
||||
this.addonLifecycleService.AddonPreFinalize -= this.AddonPreFinalizeForward;
|
||||
foreach (var listener in this.eventListeners)
|
||||
{
|
||||
this.addonLifecycleService.UnregisterListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddonPreSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPreSetup?.Invoke(args);
|
||||
/// <inheritdoc/>
|
||||
public void RegisterListener(AddonEvent eventType, IEnumerable<string> addonNames, IAddonLifecycle.AddonEventDelegate handler)
|
||||
{
|
||||
foreach (var addonName in addonNames)
|
||||
{
|
||||
this.RegisterListener(eventType, addonName, handler);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddonPostSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPostSetup?.Invoke(args);
|
||||
/// <inheritdoc/>
|
||||
public void RegisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate handler)
|
||||
{
|
||||
var listener = new AddonLifecycleEventListener(eventType, addonName, handler);
|
||||
this.eventListeners.Add(listener);
|
||||
this.addonLifecycleService.RegisterListener(listener);
|
||||
}
|
||||
|
||||
private void AddonPreFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPreFinalize?.Invoke(args);
|
||||
/// <inheritdoc/>
|
||||
public void RegisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate handler)
|
||||
{
|
||||
this.RegisterListener(eventType, string.Empty, handler);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterListener(AddonEvent eventType, IEnumerable<string> addonNames, IAddonLifecycle.AddonEventDelegate? handler = null)
|
||||
{
|
||||
foreach (var addonName in addonNames)
|
||||
{
|
||||
this.UnregisterListener(eventType, addonName, handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate? handler = null)
|
||||
{
|
||||
this.eventListeners.RemoveAll(entry =>
|
||||
{
|
||||
if (entry.EventType != eventType) return false;
|
||||
if (entry.AddonName != addonName) return false;
|
||||
if (handler is not null && entry.FunctionDelegate != handler) return false;
|
||||
|
||||
this.addonLifecycleService.UnregisterListener(entry);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate? handler = null)
|
||||
{
|
||||
this.UnregisterListener(eventType, string.Empty, handler);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnregisterListener(params IAddonLifecycle.AddonEventDelegate[] handlers)
|
||||
{
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
this.eventListeners.RemoveAll(entry =>
|
||||
{
|
||||
if (entry.FunctionDelegate != handler) return false;
|
||||
|
||||
this.addonLifecycleService.UnregisterListener(entry);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,35 @@
|
|||
internal class AddonLifecycleAddressResolver : BaseAddressResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the addon setup hook invoked by the atkunitmanager.
|
||||
/// Gets the address of the addon setup hook invoked by the AtkUnitManager.
|
||||
/// </summary>
|
||||
public nint AddonSetup { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon finalize hook invoked by the atkunitmanager.
|
||||
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
|
||||
/// </summary>
|
||||
public nint AddonFinalize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon draw hook invoked by virtual function call.
|
||||
/// </summary>
|
||||
public nint AddonDraw { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon update hook invoked by virtual function call.
|
||||
/// </summary>
|
||||
public nint AddonUpdate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
|
||||
/// </summary>
|
||||
public nint AddonOnRequestedUpdate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of AtkUnitManager_vf10 which triggers addon onRefresh.
|
||||
/// </summary>
|
||||
public nint AddonOnRefresh { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan for and setup any configured address pointers.
|
||||
/// </summary>
|
||||
|
|
@ -23,5 +43,9 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver
|
|||
{
|
||||
this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14");
|
||||
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");
|
||||
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 90 01 00 00 48 8B 5C 24 30 48 83 C4 20");
|
||||
this.AddonOnRefresh = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 41 8B F8 48 8B DA");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs
Normal file
38
Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Dalamud.Game.AddonLifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// This class is a helper for tracking and invoking listener delegates.
|
||||
/// </summary>
|
||||
internal class AddonLifecycleEventListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonLifecycleEventListener"/> class.
|
||||
/// </summary>
|
||||
/// <param name="eventType">Event type to listen for.</param>
|
||||
/// <param name="addonName">Addon name to listen for.</param>
|
||||
/// <param name="functionDelegate">Delegate to invoke.</param>
|
||||
internal AddonLifecycleEventListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate functionDelegate)
|
||||
{
|
||||
this.EventType = eventType;
|
||||
this.AddonName = addonName;
|
||||
this.FunctionDelegate = functionDelegate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon this listener is looking for.
|
||||
/// string.Empty if it wants to be called for any addon.
|
||||
/// </summary>
|
||||
public string AddonName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the event type this listener is looking for.
|
||||
/// </summary>
|
||||
public AddonEvent EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delegate this listener invokes.
|
||||
/// </summary>
|
||||
public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; }
|
||||
}
|
||||
89
Dalamud/Hooking/Internal/CallHook.cs
Normal file
89
Dalamud/Hooking/Internal/CallHook.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Reloaded.Hooks.Definitions;
|
||||
|
||||
namespace Dalamud.Hooking.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook.
|
||||
/// This is a destructive operation, no other callsite hooks can coexist at the same address.
|
||||
///
|
||||
/// There's no .Original for this hook type.
|
||||
/// This is only intended for be for functions where the parameters provided allow you to invoke the original call.
|
||||
///
|
||||
/// This class was specifically added for hooking virtual function callsites.
|
||||
/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
|
||||
internal class CallHook<T> : IDisposable where T : Delegate
|
||||
{
|
||||
private readonly Reloaded.Hooks.AsmHook asmHook;
|
||||
|
||||
private T? detour;
|
||||
private bool activated;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CallHook{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the instruction to replace.</param>
|
||||
/// <param name="detour">Delegate to invoke.</param>
|
||||
internal CallHook(nint address, T detour)
|
||||
{
|
||||
this.detour = detour;
|
||||
|
||||
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
|
||||
var code = new[]
|
||||
{
|
||||
"use64",
|
||||
$"mov rax, 0x{detourPtr:X8}",
|
||||
"call rax",
|
||||
};
|
||||
|
||||
var opt = new AsmHookOptions
|
||||
{
|
||||
PreferRelativeJump = true,
|
||||
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
|
||||
MaxOpcodeSize = 5,
|
||||
};
|
||||
|
||||
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the hook is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled => this.asmHook.IsEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Starts intercepting a call to the function.
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
if (!this.activated)
|
||||
{
|
||||
this.activated = true;
|
||||
this.asmHook.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.asmHook.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops intercepting a call to the function.
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
this.asmHook.Disable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a hook from the current process.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.asmHook.Disable();
|
||||
this.detour = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.AddonLifecycle;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
|
||||
|
||||
/// <summary>
|
||||
/// Test setup AddonLifecycle Service.
|
||||
/// </summary>
|
||||
internal class AddonLifecycleAgingStep : IAgingStep
|
||||
{
|
||||
private readonly List<AddonLifecycleEventListener> listeners;
|
||||
|
||||
private AddonLifecycle? service;
|
||||
private TestStep currentStep = TestStep.CharacterRefresh;
|
||||
private bool listenersRegistered;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonLifecycleAgingStep"/> class.
|
||||
/// </summary>
|
||||
public AddonLifecycleAgingStep()
|
||||
{
|
||||
this.listeners = new List<AddonLifecycleEventListener>
|
||||
{
|
||||
new(AddonEvent.PostSetup, "Character", this.PostSetup),
|
||||
new(AddonEvent.PostUpdate, "Character", this.PostUpdate),
|
||||
new(AddonEvent.PostDraw, "Character", this.PostDraw),
|
||||
new(AddonEvent.PostRefresh, "Character", this.PostRefresh),
|
||||
new(AddonEvent.PostRequestedUpdate, "Character", this.PostRequestedUpdate),
|
||||
new(AddonEvent.PreFinalize, "Character", this.PreFinalize),
|
||||
};
|
||||
}
|
||||
|
||||
private enum TestStep
|
||||
{
|
||||
CharacterRefresh,
|
||||
CharacterSetup,
|
||||
CharacterRequestedUpdate,
|
||||
CharacterUpdate,
|
||||
CharacterDraw,
|
||||
CharacterFinalize,
|
||||
Complete,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Test AddonLifecycle";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SelfTestStepResult RunStep()
|
||||
{
|
||||
this.service ??= Service<AddonLifecycle>.Get();
|
||||
if (this.service is null) return SelfTestStepResult.Fail;
|
||||
|
||||
if (!this.listenersRegistered)
|
||||
{
|
||||
foreach (var listener in this.listeners)
|
||||
{
|
||||
this.service.RegisterListener(listener);
|
||||
}
|
||||
|
||||
this.listenersRegistered = true;
|
||||
}
|
||||
|
||||
switch (this.currentStep)
|
||||
{
|
||||
case TestStep.CharacterRefresh:
|
||||
ImGui.Text("Open Character Window.");
|
||||
break;
|
||||
|
||||
case TestStep.CharacterSetup:
|
||||
ImGui.Text("Open Character Window.");
|
||||
break;
|
||||
|
||||
case TestStep.CharacterRequestedUpdate:
|
||||
ImGui.Text("Change tabs, or un-equip/equip gear.");
|
||||
break;
|
||||
|
||||
case TestStep.CharacterFinalize:
|
||||
ImGui.Text("Close Character Window.");
|
||||
break;
|
||||
|
||||
case TestStep.CharacterUpdate:
|
||||
case TestStep.CharacterDraw:
|
||||
case TestStep.Complete:
|
||||
default:
|
||||
// Nothing to report to tester.
|
||||
break;
|
||||
}
|
||||
|
||||
return this.currentStep is TestStep.Complete ? SelfTestStepResult.Pass : SelfTestStepResult.Waiting;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CleanUp()
|
||||
{
|
||||
foreach (var listener in this.listeners)
|
||||
{
|
||||
this.service?.UnregisterListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void PostSetup(AddonEvent eventType, AddonArgs addonInfo)
|
||||
{
|
||||
if (this.currentStep is TestStep.CharacterSetup) this.currentStep++;
|
||||
}
|
||||
|
||||
private void PostUpdate(AddonEvent eventType, AddonArgs addonInfo)
|
||||
{
|
||||
if (this.currentStep is TestStep.CharacterUpdate) this.currentStep++;
|
||||
}
|
||||
|
||||
private void PostDraw(AddonEvent eventType, AddonArgs addonInfo)
|
||||
{
|
||||
if (this.currentStep is TestStep.CharacterDraw) this.currentStep++;
|
||||
}
|
||||
|
||||
private void PostRefresh(AddonEvent eventType, AddonArgs addonInfo)
|
||||
{
|
||||
if (this.currentStep is TestStep.CharacterRefresh) this.currentStep++;
|
||||
}
|
||||
|
||||
private void PostRequestedUpdate(AddonEvent eventType, AddonArgs addonInfo)
|
||||
{
|
||||
if (this.currentStep is TestStep.CharacterRequestedUpdate) this.currentStep++;
|
||||
}
|
||||
|
||||
private void PreFinalize(AddonEvent eventType, AddonArgs addonInfo)
|
||||
{
|
||||
if (this.currentStep is TestStep.CharacterFinalize) this.currentStep++;
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ internal class SelfTestWindow : Window
|
|||
new ChatAgingStep(),
|
||||
new HoverAgingStep(),
|
||||
new LuminaAgingStep<TerritoryType>(),
|
||||
new AddonLifecycleAgingStep(),
|
||||
new PartyFinderAgingStep(),
|
||||
new HandledExceptionAgingStep(),
|
||||
new DutyStateAgingStep(),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Dalamud.Game.AddonLifecycle;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
|
|
@ -11,35 +11,70 @@ namespace Dalamud.Plugin.Services;
|
|||
public interface IAddonLifecycle
|
||||
{
|
||||
/// <summary>
|
||||
/// Event that fires before an addon is being setup.
|
||||
/// Delegate for receiving addon lifecycle event messages.
|
||||
/// </summary>
|
||||
public event Action<AddonArgs> AddonPreSetup;
|
||||
/// <param name="eventType">The event type that triggered the message.</param>
|
||||
/// <param name="addonInfo">Information about what addon triggered the message.</param>
|
||||
public delegate void AddonEventDelegate(AddonEvent eventType, AddonArgs addonInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Event that fires after an addon is done being setup.
|
||||
/// Register a listener that will trigger on the specified event and any of the specified addons.
|
||||
/// </summary>
|
||||
public event Action<AddonArgs> AddonPostSetup;
|
||||
/// <param name="eventType">Event type to trigger on.</param>
|
||||
/// <param name="addonNames">Addon names that will trigger the handler to be invoked.</param>
|
||||
/// <param name="handler">The handler to invoke.</param>
|
||||
void RegisterListener(AddonEvent eventType, IEnumerable<string> addonNames, AddonEventDelegate handler);
|
||||
|
||||
/// <summary>
|
||||
/// Event that fires before an addon is being finalized.
|
||||
/// Register a listener that will trigger on the specified event only for the specified addon.
|
||||
/// </summary>
|
||||
public event Action<AddonArgs> AddonPreFinalize;
|
||||
/// <param name="eventType">Event type to trigger on.</param>
|
||||
/// <param name="addonName">The addon name that will trigger the handler to be invoked.</param>
|
||||
/// <param name="handler">The handler to invoke.</param>
|
||||
void RegisterListener(AddonEvent eventType, string addonName, AddonEventDelegate handler);
|
||||
|
||||
/// <summary>
|
||||
/// Addon argument data for use in event subscribers.
|
||||
/// Register a listener that will trigger on the specified event for any addon.
|
||||
/// </summary>
|
||||
public unsafe class AddonArgs
|
||||
{
|
||||
private string? addonName;
|
||||
/// <param name="eventType">Event type to trigger on.</param>
|
||||
/// <param name="handler">The handler to invoke.</param>
|
||||
void RegisterListener(AddonEvent eventType, AddonEventDelegate handler);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon this args referrers to.
|
||||
/// </summary>
|
||||
public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20);
|
||||
/// <summary>
|
||||
/// Unregister listener from specified event type and specified addon names.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a specific handler is not provided, all handlers for the event type and addon names will be unregistered.
|
||||
/// </remarks>
|
||||
/// <param name="eventType">Event type to deregister.</param>
|
||||
/// <param name="addonNames">Addon names to deregister.</param>
|
||||
/// <param name="handler">Optional specific handler to remove.</param>
|
||||
void UnregisterListener(AddonEvent eventType, IEnumerable<string> addonNames, [Optional] AddonEventDelegate handler);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pointer to the addons AtkUnitBase.
|
||||
/// </summary>
|
||||
required public nint Addon { get; init; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Unregister all listeners for the specified event type and addon name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a specific handler is not provided, all handlers for the event type and addons will be unregistered.
|
||||
/// </remarks>
|
||||
/// <param name="eventType">Event type to deregister.</param>
|
||||
/// <param name="addonName">Addon name to deregister.</param>
|
||||
/// <param name="handler">Optional specific handler to remove.</param>
|
||||
void UnregisterListener(AddonEvent eventType, string addonName, [Optional] AddonEventDelegate handler);
|
||||
|
||||
/// <summary>
|
||||
/// Unregister an event type handler.<br/>This will only remove a handler that is added via <see cref="RegisterListener(AddonEvent, AddonEventDelegate)"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a specific handler is not provided, all handlers for the event type and addons will be unregistered.
|
||||
/// </remarks>
|
||||
/// <param name="eventType">Event type to deregister.</param>
|
||||
/// <param name="handler">Optional specific handler to remove.</param>
|
||||
void UnregisterListener(AddonEvent eventType, [Optional] AddonEventDelegate handler);
|
||||
|
||||
/// <summary>
|
||||
/// Unregister all events that use the specified handlers.
|
||||
/// </summary>
|
||||
/// <param name="handlers">Handlers to remove.</param>
|
||||
void UnregisterListener(params AddonEventDelegate[] handlers);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue