mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-20 14:57:45 +01:00
Merge remote-tracking branch 'upstream/master' into feature/inotificationmanager
This commit is contained in:
commit
033a57d19d
51 changed files with 3594 additions and 1284 deletions
107
Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
Normal file
107
Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
namespace Dalamud.Game.Addon;
|
||||
|
||||
/// <summary>Argument pool for Addon Lifecycle services.</summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class AddonLifecyclePooledArgs : IServiceType
|
||||
{
|
||||
private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
|
||||
private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
|
||||
private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
|
||||
private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
|
||||
private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
|
||||
private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
|
||||
private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonLifecyclePooledArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonSetupArgs> Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonFinalizeArgs> Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonDrawArgs> Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonUpdateArgs> Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonRefreshArgs> Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonRequestedUpdateArgs> Rent(out AddonRequestedUpdateArgs arg) =>
|
||||
new(out arg, this.addonRequestedUpdateArgPool);
|
||||
|
||||
/// <summary>Rents an instance of an argument.</summary>
|
||||
/// <param name="arg">The rented instance.</param>
|
||||
/// <returns>The returner.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PooledEntry<AddonReceiveEventArgs> Rent(out AddonReceiveEventArgs arg) =>
|
||||
new(out arg, this.addonReceiveEventArgPool);
|
||||
|
||||
/// <summary>Returns the object to the pool on dispose.</summary>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
public readonly ref struct PooledEntry<T>
|
||||
where T : AddonArgs, new()
|
||||
{
|
||||
private readonly Span<T> pool;
|
||||
private readonly T obj;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="PooledEntry{T}"/> struct.</summary>
|
||||
/// <param name="arg">An instance of the argument.</param>
|
||||
/// <param name="pool">The pool to rent from and return to.</param>
|
||||
public PooledEntry(out T arg, Span<T> pool)
|
||||
{
|
||||
this.pool = pool;
|
||||
foreach (ref var item in pool)
|
||||
{
|
||||
if (Interlocked.Exchange(ref item, null) is { } v)
|
||||
{
|
||||
this.obj = arg = v;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.obj = arg = new();
|
||||
}
|
||||
|
||||
/// <summary>Returns the item to the pool.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
var tmp = this.obj;
|
||||
foreach (ref var item in this.pool)
|
||||
{
|
||||
if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
|
||||
return;
|
||||
tmp = tmp2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Events;
|
|||
/// Service provider for addon event management.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class AddonEventManager : IDisposable, IServiceType
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ public abstract unsafe class AddonArgs
|
|||
get => this.addon;
|
||||
set
|
||||
{
|
||||
if (this.addon == value)
|
||||
return;
|
||||
|
||||
this.addon = value;
|
||||
|
||||
// Note: always clear addonName on updating the addon being pointed.
|
||||
// Same address may point to a different addon.
|
||||
this.addonName = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -19,7 +18,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
|
|||
/// This class provides events for in-game addon lifecycles.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
||||
{
|
||||
private static readonly ModuleLog Log = new("AddonLifecycle");
|
||||
|
|
@ -27,6 +26,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
|
||||
|
||||
private readonly nint disallowedReceiveEventAddress;
|
||||
|
||||
private readonly AddonLifecycleAddressResolver address;
|
||||
|
|
@ -38,18 +40,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
private readonly Hook<AddonOnRefreshDelegate> onAddonRefreshHook;
|
||||
private readonly CallHook<AddonOnRequestedUpdateDelegate> onAddonRequestedUpdateHook;
|
||||
|
||||
// Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
|
||||
// package, and these events are always called from the main thread, this is fine.
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// TODO: turn constructors of these internal
|
||||
private readonly AddonSetupArgs recyclingSetupArgs = new();
|
||||
private readonly AddonFinalizeArgs recyclingFinalizeArgs = new();
|
||||
private readonly AddonDrawArgs recyclingDrawArgs = new();
|
||||
private readonly AddonUpdateArgs recyclingUpdateArgs = new();
|
||||
private readonly AddonRefreshArgs recyclingRefreshArgs = new();
|
||||
private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private AddonLifecycle(TargetSigScanner sigScanner)
|
||||
{
|
||||
|
|
@ -253,12 +243,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
|
||||
}
|
||||
|
||||
this.recyclingSetupArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingSetupArgs.AtkValueCount = valueCount;
|
||||
this.recyclingSetupArgs.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs);
|
||||
valueCount = this.recyclingSetupArgs.AtkValueCount;
|
||||
values = (AtkValue*)this.recyclingSetupArgs.AtkValues;
|
||||
using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
|
||||
arg.AddonInternal = (nint)addon;
|
||||
arg.AtkValueCount = valueCount;
|
||||
arg.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
|
||||
valueCount = arg.AtkValueCount;
|
||||
values = (AtkValue*)arg.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -269,7 +260,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostSetup, this.recyclingSetupArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
|
||||
}
|
||||
|
||||
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
|
||||
|
|
@ -284,8 +275,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
|
||||
}
|
||||
|
||||
this.recyclingFinalizeArgs.AddonInternal = (nint)atkUnitBase[0];
|
||||
this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs);
|
||||
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
|
||||
arg.AddonInternal = (nint)atkUnitBase[0];
|
||||
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -299,8 +291,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
|
||||
private void OnAddonDraw(AtkUnitBase* addon)
|
||||
{
|
||||
this.recyclingDrawArgs.AddonInternal = (nint)addon;
|
||||
this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs);
|
||||
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
|
||||
arg.AddonInternal = (nint)addon;
|
||||
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -311,14 +304,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostDraw, this.recyclingDrawArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
|
||||
}
|
||||
|
||||
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
|
||||
{
|
||||
this.recyclingUpdateArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingUpdateArgs.TimeDeltaInternal = delta;
|
||||
this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs);
|
||||
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
|
||||
arg.AddonInternal = (nint)addon;
|
||||
arg.TimeDeltaInternal = delta;
|
||||
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -329,19 +323,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostUpdate, this.recyclingUpdateArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
|
||||
}
|
||||
|
||||
private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
|
||||
{
|
||||
byte result = 0;
|
||||
|
||||
this.recyclingRefreshArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingRefreshArgs.AtkValueCount = valueCount;
|
||||
this.recyclingRefreshArgs.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs);
|
||||
valueCount = this.recyclingRefreshArgs.AtkValueCount;
|
||||
values = (AtkValue*)this.recyclingRefreshArgs.AtkValues;
|
||||
using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
|
||||
arg.AddonInternal = (nint)addon;
|
||||
arg.AtkValueCount = valueCount;
|
||||
arg.AtkValues = (nint)values;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
|
||||
valueCount = arg.AtkValueCount;
|
||||
values = (AtkValue*)arg.AtkValues;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -352,18 +347,19 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostRefresh, this.recyclingRefreshArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
||||
{
|
||||
this.recyclingRequestedUpdateArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
|
||||
this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs);
|
||||
numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData;
|
||||
stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.StringArrayData;
|
||||
using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
|
||||
arg.AddonInternal = (nint)addon;
|
||||
arg.NumberArrayData = (nint)numberArrayData;
|
||||
arg.StringArrayData = (nint)stringArrayData;
|
||||
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
|
||||
numberArrayData = (NumberArrayData**)arg.NumberArrayData;
|
||||
stringArrayData = (StringArrayData**)arg.StringArrayData;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -374,7 +370,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.recyclingRequestedUpdateArgs);
|
||||
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,8 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
|||
{
|
||||
private static readonly ModuleLog Log = new("AddonLifecycle");
|
||||
|
||||
// Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
|
||||
// package, and these events are always called from the main thread, this is fine.
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// TODO: turn constructors of these internal
|
||||
private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
|
||||
|
|
@ -82,16 +78,17 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
this.recyclingReceiveEventArgs.AddonInternal = (nint)addon;
|
||||
this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType;
|
||||
this.recyclingReceiveEventArgs.EventParam = eventParam;
|
||||
this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent;
|
||||
this.recyclingReceiveEventArgs.Data = data;
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs);
|
||||
eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType;
|
||||
eventParam = this.recyclingReceiveEventArgs.EventParam;
|
||||
atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent;
|
||||
data = this.recyclingReceiveEventArgs.Data;
|
||||
using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
|
||||
arg.AddonInternal = (nint)addon;
|
||||
arg.AtkEventType = (byte)eventType;
|
||||
arg.EventParam = eventParam;
|
||||
arg.AtkEvent = (IntPtr)atkEvent;
|
||||
arg.Data = data;
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
|
||||
eventType = (AtkEventType)arg.AtkEventType;
|
||||
eventParam = arg.EventParam;
|
||||
atkEvent = (AtkEvent*)arg.AtkEvent;
|
||||
data = arg.Data;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -102,6 +99,6 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
|
|||
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
|
||||
}
|
||||
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.recyclingReceiveEventArgs);
|
||||
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
|
||||
using Dalamud.Game.ClientState.Statuses;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
|
|
@ -57,8 +58,22 @@ public unsafe class BattleChara : Character
|
|||
/// <summary>
|
||||
/// Gets the total casting time of the spell being cast by the chara.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This can only be a portion of the total cast for some actions.
|
||||
/// Use AdjustedTotalCastTime if you always need the total cast time.
|
||||
/// </remarks>
|
||||
[Api10ToDo("Rename so it is not confused with AdjustedTotalCastTime")]
|
||||
public float TotalCastTime => this.Struct->GetCastInfo->TotalCastTime;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="TotalCastTime"/> plus any adjustments from the game, such as Action offset 2B. Used for display purposes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the actual total cast time for all actions.
|
||||
/// </remarks>
|
||||
[Api10ToDo("Rename so it is not confused with TotalCastTime")]
|
||||
public float AdjustedTotalCastTime => this.Struct->GetCastInfo->AdjustedTotalCastTime;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying structure.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
|
@ -41,11 +42,13 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
private readonly object runOnNextTickTaskListSync = new();
|
||||
private List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
|
||||
private List<RunOnNextTickTaskBase> runOnNextTickTaskList2 = new();
|
||||
private readonly CancellationTokenSource frameworkDestroy;
|
||||
private readonly ThreadBoundTaskScheduler frameworkThreadTaskScheduler;
|
||||
|
||||
private Thread? frameworkUpdateThread;
|
||||
private readonly ConcurrentDictionary<TaskCompletionSource, (ulong Expire, CancellationToken CancellationToken)>
|
||||
tickDelayedTaskCompletionSources = new();
|
||||
|
||||
private ulong tickCounter;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private Framework(TargetSigScanner sigScanner, GameLifecycle lifecycle)
|
||||
|
|
@ -56,6 +59,14 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
this.addressResolver = new FrameworkAddressResolver();
|
||||
this.addressResolver.Setup(sigScanner);
|
||||
|
||||
this.frameworkDestroy = new();
|
||||
this.frameworkThreadTaskScheduler = new();
|
||||
this.FrameworkThreadTaskFactory = new(
|
||||
this.frameworkDestroy.Token,
|
||||
TaskCreationOptions.None,
|
||||
TaskContinuationOptions.None,
|
||||
this.frameworkThreadTaskScheduler);
|
||||
|
||||
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
|
||||
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
|
||||
|
||||
|
|
@ -92,14 +103,17 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
/// <inheritdoc/>
|
||||
public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TaskFactory FrameworkThreadTaskFactory { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread;
|
||||
public bool IsInFrameworkUpdateThread => this.frameworkThreadTaskScheduler.IsOnBoundThread;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsFrameworkUnloading { get; internal set; }
|
||||
public bool IsFrameworkUnloading => this.frameworkDestroy.IsCancellationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of update sub-delegates that didn't get updated this frame.
|
||||
|
|
@ -111,6 +125,19 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
/// </summary>
|
||||
internal bool DispatchUpdateEvents { get; set; } = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this.frameworkDestroy.IsCancellationRequested)
|
||||
return Task.FromCanceled(this.frameworkDestroy.Token);
|
||||
if (numTicks <= 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var tcs = new TaskCompletionSource();
|
||||
this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<T> func) =>
|
||||
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func);
|
||||
|
|
@ -157,20 +184,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled<T>(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<T>();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<T>()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Func = func,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => func(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -186,20 +209,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Action = action,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => action(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -215,20 +234,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled<T>(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<Task<T>>();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task<T>>()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Func = func,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap();
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => func(),
|
||||
cancellationToken).Unwrap();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -244,20 +259,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<Task>();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task>()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Func = func,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap();
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => func(),
|
||||
cancellationToken).Unwrap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -333,23 +344,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
}
|
||||
}
|
||||
|
||||
private void RunPendingTickTasks()
|
||||
{
|
||||
if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
(this.runOnNextTickTaskList, this.runOnNextTickTaskList2) = (this.runOnNextTickTaskList2, this.runOnNextTickTaskList);
|
||||
|
||||
this.runOnNextTickTaskList2.RemoveAll(x => x.Run());
|
||||
}
|
||||
}
|
||||
|
||||
private bool HandleFrameworkUpdate(IntPtr framework)
|
||||
{
|
||||
this.frameworkUpdateThread ??= Thread.CurrentThread;
|
||||
this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread;
|
||||
|
||||
ThreadSafety.MarkMainThread();
|
||||
|
||||
|
|
@ -381,18 +378,30 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
this.LastUpdate = DateTime.Now;
|
||||
this.LastUpdateUTC = DateTime.UtcNow;
|
||||
this.tickCounter++;
|
||||
foreach (var (k, (expiry, ct)) in this.tickDelayedTaskCompletionSources)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
k.SetCanceled(ct);
|
||||
else if (expiry <= this.tickCounter)
|
||||
k.SetResult();
|
||||
else
|
||||
continue;
|
||||
|
||||
this.tickDelayedTaskCompletionSources.Remove(k, out _);
|
||||
}
|
||||
|
||||
if (StatsEnabled)
|
||||
{
|
||||
StatsStopwatch.Restart();
|
||||
this.RunPendingTickTasks();
|
||||
this.frameworkThreadTaskScheduler.Run();
|
||||
StatsStopwatch.Stop();
|
||||
|
||||
AddToStats(nameof(this.RunPendingTickTasks), StatsStopwatch.Elapsed.TotalMilliseconds);
|
||||
AddToStats(nameof(this.frameworkThreadTaskScheduler), StatsStopwatch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RunPendingTickTasks();
|
||||
this.frameworkThreadTaskScheduler.Run();
|
||||
}
|
||||
|
||||
if (StatsEnabled && this.Update != null)
|
||||
|
|
@ -404,7 +413,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
// Cleanup handlers that are no longer being called
|
||||
foreach (var key in this.NonUpdatedSubDelegates)
|
||||
{
|
||||
if (key == nameof(this.RunPendingTickTasks))
|
||||
if (key == nameof(this.FrameworkThreadTaskFactory))
|
||||
continue;
|
||||
|
||||
if (StatsHistory[key].Count > 0)
|
||||
|
|
@ -431,8 +440,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
private bool HandleFrameworkDestroy(IntPtr framework)
|
||||
{
|
||||
this.IsFrameworkUnloading = true;
|
||||
this.frameworkDestroy.Cancel();
|
||||
this.DispatchUpdateEvents = false;
|
||||
foreach (var k in this.tickDelayedTaskCompletionSources.Keys)
|
||||
k.SetCanceled(this.frameworkDestroy.Token);
|
||||
this.tickDelayedTaskCompletionSources.Clear();
|
||||
|
||||
// All the same, for now...
|
||||
this.lifecycle.SetShuttingDown();
|
||||
|
|
@ -440,95 +452,12 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
Log.Information("Framework::Destroy!");
|
||||
Service<Dalamud>.Get().Unload();
|
||||
this.RunPendingTickTasks();
|
||||
this.frameworkThreadTaskScheduler.Run();
|
||||
ServiceManager.WaitForServiceUnload();
|
||||
Log.Information("Framework::Destroy OK!");
|
||||
|
||||
return this.destroyHook.OriginalDisposeSafe(framework);
|
||||
}
|
||||
|
||||
private abstract class RunOnNextTickTaskBase
|
||||
{
|
||||
internal int RemainingTicks { get; set; }
|
||||
|
||||
internal long RunAfterTickCount { get; init; }
|
||||
|
||||
internal CancellationToken CancellationToken { get; init; }
|
||||
|
||||
internal bool Run()
|
||||
{
|
||||
if (this.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
this.CancelImpl();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.RemainingTicks > 0)
|
||||
this.RemainingTicks -= 1;
|
||||
if (this.RemainingTicks > 0)
|
||||
return false;
|
||||
|
||||
if (this.RunAfterTickCount > Environment.TickCount64)
|
||||
return false;
|
||||
|
||||
this.RunImpl();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract void RunImpl();
|
||||
|
||||
protected abstract void CancelImpl();
|
||||
}
|
||||
|
||||
private class RunOnNextTickTaskFunc<T> : RunOnNextTickTaskBase
|
||||
{
|
||||
internal TaskCompletionSource<T> TaskCompletionSource { get; init; }
|
||||
|
||||
internal Func<T> Func { get; init; }
|
||||
|
||||
protected override void RunImpl()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.TaskCompletionSource.SetResult(this.Func());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.TaskCompletionSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CancelImpl()
|
||||
{
|
||||
this.TaskCompletionSource.SetCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
private class RunOnNextTickTaskAction : RunOnNextTickTaskBase
|
||||
{
|
||||
internal TaskCompletionSource TaskCompletionSource { get; init; }
|
||||
|
||||
internal Action Action { get; init; }
|
||||
|
||||
protected override void RunImpl()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Action();
|
||||
this.TaskCompletionSource.SetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.TaskCompletionSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CancelImpl()
|
||||
{
|
||||
this.TaskCompletionSource.SetCanceled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -561,7 +490,10 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
|
|||
|
||||
/// <inheritdoc/>
|
||||
public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TaskFactory FrameworkThreadTaskFactory => this.frameworkService.FrameworkThreadTaskFactory;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta;
|
||||
|
||||
|
|
@ -579,6 +511,10 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
|
|||
this.Update = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) =>
|
||||
this.frameworkService.DelayTicks(numTicks, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<T> func)
|
||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||
|
|
|
|||
560
Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
Normal file
560
Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
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 Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// This class handles interacting with the game's (right-click) context menu.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu
|
||||
{
|
||||
private static readonly ModuleLog Log = new("ContextMenu");
|
||||
|
||||
private readonly Hook<RaptureAtkModuleOpenAddonByAgentDelegate> raptureAtkModuleOpenAddonByAgentHook;
|
||||
private readonly Hook<AddonContextMenuOnMenuSelectedDelegate> addonContextMenuOnMenuSelectedHook;
|
||||
private readonly RaptureAtkModuleOpenAddonDelegate raptureAtkModuleOpenAddon;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ContextMenu()
|
||||
{
|
||||
this.raptureAtkModuleOpenAddonByAgentHook = Hook<RaptureAtkModuleOpenAddonByAgentDelegate>.FromAddress((nint)RaptureAtkModule.Addresses.OpenAddonByAgent.Value, this.RaptureAtkModuleOpenAddonByAgentDetour);
|
||||
this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenuOnMenuSelectedDelegate>.FromAddress((nint)AddonContextMenu.StaticVTable.OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour);
|
||||
this.raptureAtkModuleOpenAddon = Marshal.GetDelegateForFunctionPointer<RaptureAtkModuleOpenAddonDelegate>((nint)RaptureAtkModule.Addresses.OpenAddon.Value);
|
||||
|
||||
this.raptureAtkModuleOpenAddonByAgentHook.Enable();
|
||||
this.addonContextMenuOnMenuSelectedHook.Enable();
|
||||
}
|
||||
|
||||
private unsafe delegate ushort RaptureAtkModuleOpenAddonByAgentDelegate(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId);
|
||||
|
||||
private unsafe delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
|
||||
|
||||
private unsafe delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
|
||||
|
||||
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
|
||||
|
||||
private object MenuItemsLock { get; } = new();
|
||||
|
||||
private AgentInterface* SelectedAgent { get; set; }
|
||||
|
||||
private ContextMenuType? SelectedMenuType { get; set; }
|
||||
|
||||
private List<MenuItem>? SelectedItems { get; set; }
|
||||
|
||||
private HashSet<nint> SelectedEventInterfaces { get; } = new();
|
||||
|
||||
private AtkUnitBase* SelectedParentAddon { get; set; }
|
||||
|
||||
// -1 -> -inf: native items
|
||||
// 0 -> inf: selected items
|
||||
private List<int> MenuCallbackIds { get; } = new();
|
||||
|
||||
private IReadOnlyList<MenuItem>? SubmenuItems { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
var manager = RaptureAtkUnitManager.Instance();
|
||||
var menu = manager->GetAddonByName("ContextMenu");
|
||||
var submenu = manager->GetAddonByName("AddonContextSub");
|
||||
if (menu->IsVisible)
|
||||
menu->FireCallbackInt(-1);
|
||||
if (submenu->IsVisible)
|
||||
submenu->FireCallbackInt(-1);
|
||||
|
||||
this.raptureAtkModuleOpenAddonByAgentHook.Dispose();
|
||||
this.addonContextMenuOnMenuSelectedHook.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.MenuItems[menuType] = items = new();
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
return false;
|
||||
return items.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
private AtkValue* ExpandContextMenuArray(Span<AtkValue> oldValues, int newSize)
|
||||
{
|
||||
// if the array has enough room, don't reallocate
|
||||
if (oldValues.Length >= newSize)
|
||||
return (AtkValue*)Unsafe.AsPointer(ref oldValues[0]);
|
||||
|
||||
var size = (sizeof(AtkValue) * newSize) + 8;
|
||||
var newArray = (nint)IMemorySpace.GetUISpace()->Malloc((ulong)size, 0);
|
||||
if (newArray == nint.Zero)
|
||||
throw new OutOfMemoryException();
|
||||
NativeMemory.Fill((void*)newArray, (nuint)size, 0);
|
||||
|
||||
*(ulong*)newArray = (ulong)newSize;
|
||||
|
||||
// copy old memory if existing
|
||||
if (!oldValues.IsEmpty)
|
||||
oldValues.CopyTo(new((void*)(newArray + 8), oldValues.Length));
|
||||
|
||||
return (AtkValue*)(newArray + 8);
|
||||
}
|
||||
|
||||
private void FreeExpandedContextMenuArray(AtkValue* newValues, int newSize) =>
|
||||
IMemorySpace.Free((void*)((nint)newValues - 8), (ulong)((newSize * sizeof(AtkValue)) + 8));
|
||||
|
||||
private AtkValue* CreateEmptySubmenuContextMenuArray(SeString name, int x, int y, out int valueCount)
|
||||
{
|
||||
// 0: UInt = ContextItemCount
|
||||
// 1: String = Name
|
||||
// 2: Int = PositionX
|
||||
// 3: Int = PositionY
|
||||
// 4: Bool = false
|
||||
// 5: UInt = ContextItemSubmenuMask
|
||||
// 6: UInt = ReturnArrowMask (_gap_0x6BC ? 1 << (ContextItemCount - 1) : 0)
|
||||
// 7: UInt = 1
|
||||
|
||||
valueCount = 8;
|
||||
var values = this.ExpandContextMenuArray(Span<AtkValue>.Empty, valueCount);
|
||||
values[0].ChangeType(ValueType.UInt);
|
||||
values[0].UInt = 0;
|
||||
values[1].ChangeType(ValueType.String);
|
||||
values[1].SetString(name.Encode().NullTerminate());
|
||||
values[2].ChangeType(ValueType.Int);
|
||||
values[2].Int = x;
|
||||
values[3].ChangeType(ValueType.Int);
|
||||
values[3].Int = y;
|
||||
values[4].ChangeType(ValueType.Bool);
|
||||
values[4].Byte = 0;
|
||||
values[5].ChangeType(ValueType.UInt);
|
||||
values[5].UInt = 0;
|
||||
values[6].ChangeType(ValueType.UInt);
|
||||
values[6].UInt = 0;
|
||||
values[7].ChangeType(ValueType.UInt);
|
||||
values[7].UInt = 1;
|
||||
return values;
|
||||
}
|
||||
|
||||
private void SetupGenericMenu(int headerCount, int sizeHeaderIdx, int returnHeaderIdx, int submenuHeaderIdx, IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
var itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority);
|
||||
var prefixItems = itemsWithIdx.Where(i => i.item.Priority < 0).ToArray();
|
||||
var suffixItems = itemsWithIdx.Where(i => i.item.Priority >= 0).ToArray();
|
||||
|
||||
var nativeMenuSize = (int)values[sizeHeaderIdx].UInt;
|
||||
var prefixMenuSize = prefixItems.Length;
|
||||
var suffixMenuSize = suffixItems.Length;
|
||||
|
||||
var hasGameDisabled = valueCount - headerCount - nativeMenuSize > 0;
|
||||
|
||||
var hasCustomDisabled = items.Any(item => !item.IsEnabled);
|
||||
var hasAnyDisabled = hasGameDisabled || hasCustomDisabled;
|
||||
|
||||
values = this.ExpandContextMenuArray(
|
||||
new(values, valueCount),
|
||||
valueCount = (nativeMenuSize + items.Count) * (hasAnyDisabled ? 2 : 1) + headerCount);
|
||||
var offsetData = new Span<AtkValue>(values, headerCount);
|
||||
var nameData = new Span<AtkValue>(values + headerCount, nativeMenuSize + items.Count);
|
||||
var disabledData = hasAnyDisabled ? new Span<AtkValue>(values + headerCount + nativeMenuSize + items.Count, nativeMenuSize + items.Count) : Span<AtkValue>.Empty;
|
||||
|
||||
var returnMask = offsetData[returnHeaderIdx].UInt;
|
||||
var submenuMask = offsetData[submenuHeaderIdx].UInt;
|
||||
|
||||
nameData[..nativeMenuSize].CopyTo(nameData.Slice(prefixMenuSize, nativeMenuSize));
|
||||
if (hasAnyDisabled)
|
||||
{
|
||||
if (hasGameDisabled)
|
||||
{
|
||||
// copy old disabled data
|
||||
var oldDisabledData = new Span<AtkValue>(values + headerCount + nativeMenuSize, nativeMenuSize);
|
||||
oldDisabledData.CopyTo(disabledData.Slice(prefixMenuSize, nativeMenuSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
// enable all
|
||||
for (var i = prefixMenuSize; i < prefixMenuSize + nativeMenuSize; ++i)
|
||||
{
|
||||
disabledData[i].ChangeType(ValueType.Int);
|
||||
disabledData[i].Int = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
returnMask <<= prefixMenuSize;
|
||||
submenuMask <<= prefixMenuSize;
|
||||
|
||||
void FillData(Span<AtkValue> disabledData, Span<AtkValue> nameData, int i, MenuItem item, int idx)
|
||||
{
|
||||
this.MenuCallbackIds.Add(idx);
|
||||
|
||||
if (hasAnyDisabled)
|
||||
{
|
||||
disabledData[i].ChangeType(ValueType.Int);
|
||||
disabledData[i].Int = item.IsEnabled ? 0 : 1;
|
||||
}
|
||||
|
||||
if (item.IsReturn)
|
||||
returnMask |= 1u << i;
|
||||
if (item.IsSubmenu)
|
||||
submenuMask |= 1u << i;
|
||||
|
||||
nameData[i].ChangeType(ValueType.String);
|
||||
nameData[i].SetString(item.PrefixedName.Encode().NullTerminate());
|
||||
}
|
||||
|
||||
for (var i = 0; i < prefixMenuSize; ++i)
|
||||
{
|
||||
var (item, idx) = prefixItems[i];
|
||||
FillData(disabledData, nameData, i, item, idx);
|
||||
}
|
||||
|
||||
this.MenuCallbackIds.AddRange(Enumerable.Range(0, nativeMenuSize).Select(i => -i - 1));
|
||||
|
||||
for (var i = prefixMenuSize + nativeMenuSize; i < prefixMenuSize + nativeMenuSize + suffixMenuSize; ++i)
|
||||
{
|
||||
var (item, idx) = suffixItems[i - prefixMenuSize - nativeMenuSize];
|
||||
FillData(disabledData, nameData, i, item, idx);
|
||||
}
|
||||
|
||||
offsetData[returnHeaderIdx].UInt = returnMask;
|
||||
offsetData[submenuHeaderIdx].UInt = submenuMask;
|
||||
|
||||
offsetData[sizeHeaderIdx].UInt += (uint)items.Count;
|
||||
}
|
||||
|
||||
private void SetupContextMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
// 0: UInt = Item Count
|
||||
// 1: UInt = 0 (probably window name, just unused)
|
||||
// 2: UInt = Return Mask (?)
|
||||
// 3: UInt = Submenu Mask
|
||||
// 4: UInt = OpenAtCursorPosition ? 2 : 1
|
||||
// 5: UInt = 0
|
||||
// 6: UInt = 0
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!item.Prefix.HasValue)
|
||||
{
|
||||
item.PrefixChar = 'D';
|
||||
item.PrefixColor = 539;
|
||||
Log.Warning($"Menu item \"{item.Name}\" has no prefix, defaulting to Dalamud's. Menu items outside of a submenu must have a prefix.");
|
||||
}
|
||||
}
|
||||
|
||||
this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values);
|
||||
}
|
||||
|
||||
private void SetupContextSubMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
// 0: UInt = ContextItemCount
|
||||
// 1: skipped?
|
||||
// 2: Int = PositionX
|
||||
// 3: Int = PositionY
|
||||
// 4: Bool = false
|
||||
// 5: UInt = ContextItemSubmenuMask
|
||||
// 6: UInt = _gap_0x6BC ? 1 << (ContextItemCount - 1) : 0
|
||||
// 7: UInt = 1
|
||||
|
||||
this.SetupGenericMenu(8, 0, 6, 5, items, ref valueCount, ref values);
|
||||
}
|
||||
|
||||
private ushort RaptureAtkModuleOpenAddonByAgentDetour(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId)
|
||||
{
|
||||
var oldValues = values;
|
||||
|
||||
if (MemoryHelper.EqualsZeroTerminatedString("ContextMenu", (nint)addonName))
|
||||
{
|
||||
this.MenuCallbackIds.Clear();
|
||||
this.SelectedAgent = agent;
|
||||
this.SelectedParentAddon = module->RaptureAtkUnitManager.GetAddonById(parentAddonId);
|
||||
this.SelectedEventInterfaces.Clear();
|
||||
if (this.SelectedAgent == AgentInventoryContext.Instance())
|
||||
{
|
||||
this.SelectedMenuType = ContextMenuType.Inventory;
|
||||
}
|
||||
else if (this.SelectedAgent == AgentContext.Instance())
|
||||
{
|
||||
this.SelectedMenuType = ContextMenuType.Default;
|
||||
|
||||
var menu = AgentContext.Instance()->CurrentContextMenu;
|
||||
var handlers = new Span<Pointer<AtkEventInterface>>(menu->EventHandlerArray, 32);
|
||||
var ids = new Span<byte>(menu->EventIdArray, 32);
|
||||
var count = (int)values[0].UInt;
|
||||
handlers = handlers.Slice(7, count);
|
||||
ids = ids.Slice(7, count);
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
if (ids[i] <= 106)
|
||||
continue;
|
||||
this.SelectedEventInterfaces.Add((nint)handlers[i].Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SelectedMenuType = null;
|
||||
}
|
||||
|
||||
this.SubmenuItems = null;
|
||||
|
||||
if (this.SelectedMenuType is { } menuType)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.SelectedItems = new(items);
|
||||
else
|
||||
this.SelectedItems = new();
|
||||
}
|
||||
|
||||
var args = new MenuOpenedArgs(this.SelectedItems.Add, this.SelectedParentAddon, this.SelectedAgent, this.SelectedMenuType.Value, this.SelectedEventInterfaces);
|
||||
this.OnMenuOpened?.InvokeSafely(args);
|
||||
this.SelectedItems = this.FixupMenuList(this.SelectedItems, (int)values[0].UInt);
|
||||
this.SetupContextMenu(this.SelectedItems, ref valueCount, ref values);
|
||||
Log.Verbose($"Opening {this.SelectedMenuType} context menu with {this.SelectedItems.Count} custom items.");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SelectedItems = null;
|
||||
}
|
||||
}
|
||||
else if (MemoryHelper.EqualsZeroTerminatedString("AddonContextSub", (nint)addonName))
|
||||
{
|
||||
this.MenuCallbackIds.Clear();
|
||||
if (this.SubmenuItems != null)
|
||||
{
|
||||
this.SubmenuItems = this.FixupMenuList(this.SubmenuItems.ToList(), (int)values[0].UInt);
|
||||
|
||||
this.SetupContextSubMenu(this.SubmenuItems, ref valueCount, ref values);
|
||||
Log.Verbose($"Opening {this.SelectedMenuType} submenu with {this.SubmenuItems.Count} custom items.");
|
||||
}
|
||||
}
|
||||
|
||||
var ret = this.raptureAtkModuleOpenAddonByAgentHook.Original(module, addonName, addon, valueCount, values, agent, a7, parentAddonId);
|
||||
if (values != oldValues)
|
||||
this.FreeExpandedContextMenuArray(values, valueCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private List<MenuItem> FixupMenuList(List<MenuItem> items, int nativeMenuSize)
|
||||
{
|
||||
// The in game menu actually supports 32 items, but the last item can't have a visible submenu arrow.
|
||||
// As such, we'll only work with 31 items.
|
||||
const int MaxMenuItems = 31;
|
||||
if (items.Count + nativeMenuSize > MaxMenuItems)
|
||||
{
|
||||
Log.Warning($"Menu size exceeds {MaxMenuItems} items, truncating.");
|
||||
var orderedItems = items.OrderBy(i => i.Priority).ToArray();
|
||||
var newItems = orderedItems[..(MaxMenuItems - nativeMenuSize - 1)];
|
||||
var submenuItems = orderedItems[(MaxMenuItems - nativeMenuSize - 1)..];
|
||||
return newItems.Append(new MenuItem
|
||||
{
|
||||
Prefix = SeIconChar.BoxedLetterD,
|
||||
PrefixColor = 539,
|
||||
IsSubmenu = true,
|
||||
Priority = int.MaxValue,
|
||||
Name = $"See More ({submenuItems.Length})",
|
||||
OnClicked = a => a.OpenSubmenu(submenuItems),
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> submenuItems, int posX, int posY)
|
||||
{
|
||||
if (submenuItems.Count == 0)
|
||||
throw new ArgumentException("Submenu must not be empty", nameof(submenuItems));
|
||||
|
||||
this.SubmenuItems = submenuItems;
|
||||
|
||||
var module = RaptureAtkModule.Instance();
|
||||
var values = this.CreateEmptySubmenuContextMenuArray(name, posX, posY, out var valueCount);
|
||||
|
||||
switch (this.SelectedMenuType)
|
||||
{
|
||||
case ContextMenuType.Default:
|
||||
{
|
||||
var ownerAddonId = ((AgentContext*)this.SelectedAgent)->OwnerAddon;
|
||||
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 71, checked((ushort)ownerAddonId), 4);
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextMenuType.Inventory:
|
||||
{
|
||||
var ownerAddonId = ((AgentInventoryContext*)this.SelectedAgent)->OwnerAddonId;
|
||||
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 0, checked((ushort)ownerAddonId), 4);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Log.Warning($"Unknown context menu type (agent: {(nint)this.SelectedAgent}, cannot open submenu");
|
||||
break;
|
||||
}
|
||||
|
||||
this.FreeExpandedContextMenuArray(values, valueCount);
|
||||
}
|
||||
|
||||
private bool AddonContextMenuOnMenuSelectedDetour(AddonContextMenu* addon, int selectedIdx, byte a3)
|
||||
{
|
||||
var items = this.SubmenuItems ?? this.SelectedItems;
|
||||
if (items == null)
|
||||
goto original;
|
||||
if (this.MenuCallbackIds.Count == 0)
|
||||
goto original;
|
||||
if (selectedIdx < 0)
|
||||
goto original;
|
||||
if (selectedIdx >= this.MenuCallbackIds.Count)
|
||||
goto original;
|
||||
|
||||
var callbackId = this.MenuCallbackIds[selectedIdx];
|
||||
|
||||
if (callbackId < 0)
|
||||
{
|
||||
selectedIdx = -callbackId - 1;
|
||||
goto original;
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = items[callbackId];
|
||||
var openedSubmenu = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (item.OnClicked == null)
|
||||
throw new InvalidOperationException("Item has no OnClicked handler");
|
||||
item.OnClicked.InvokeSafely(new(
|
||||
(name, items) =>
|
||||
{
|
||||
short x, y;
|
||||
addon->AtkUnitBase.GetPosition(&x, &y);
|
||||
this.OpenSubmenu(name ?? item.Name, items, x, y);
|
||||
openedSubmenu = true;
|
||||
},
|
||||
this.SelectedParentAddon,
|
||||
this.SelectedAgent,
|
||||
this.SelectedMenuType.Value,
|
||||
this.SelectedEventInterfaces));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Error while handling context menu click");
|
||||
}
|
||||
|
||||
// Close with clicky sound
|
||||
if (!openedSubmenu)
|
||||
addon->AtkUnitBase.FireCallbackInt(-2);
|
||||
return false;
|
||||
}
|
||||
|
||||
original:
|
||||
// Eventually handled by inventorycontext here: 14022BBD0 (6.51)
|
||||
return this.addonContextMenuOnMenuSelectedHook.Original(addon, selectedIdx, a3);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-scoped version of a <see cref="ContextMenu"/> service.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.ScopedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IContextMenu>]
|
||||
#pragma warning restore SA1015
|
||||
internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ContextMenu parentService = Service<ContextMenu>.Get();
|
||||
|
||||
private ContextMenuPluginScoped()
|
||||
{
|
||||
this.parentService.OnMenuOpened += this.OnMenuOpenedForward;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
|
||||
|
||||
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
|
||||
|
||||
private object MenuItemsLock { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.parentService.OnMenuOpened -= this.OnMenuOpenedForward;
|
||||
|
||||
this.OnMenuOpened = null;
|
||||
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
foreach (var (menuType, items) in this.MenuItems)
|
||||
{
|
||||
foreach (var item in items)
|
||||
this.parentService.RemoveMenuItem(menuType, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.MenuItems[menuType] = items = new();
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
this.parentService.AddMenuItem(menuType, item);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (this.MenuItems.TryGetValue(menuType, out var items))
|
||||
items.Remove(item);
|
||||
}
|
||||
|
||||
return this.parentService.RemoveMenuItem(menuType, item);
|
||||
}
|
||||
|
||||
private void OnMenuOpenedForward(MenuOpenedArgs args) =>
|
||||
this.OnMenuOpened?.Invoke(args);
|
||||
}
|
||||
18
Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs
Normal file
18
Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// The type of context menu.
|
||||
/// Each one has a different associated <see cref="MenuTarget"/>.
|
||||
/// </summary>
|
||||
public enum ContextMenuType
|
||||
{
|
||||
/// <summary>
|
||||
/// The default context menu.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// The inventory context menu. Used when right-clicked on an item.
|
||||
/// </summary>
|
||||
Inventory,
|
||||
}
|
||||
77
Dalamud/Game/Gui/ContextMenu/MenuArgs.cs
Normal file
77
Dalamud/Game/Gui/ContextMenu/MenuArgs.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="IContextMenu"/> menu args.
|
||||
/// </summary>
|
||||
public abstract unsafe class MenuArgs
|
||||
{
|
||||
private IReadOnlySet<nint>? eventInterfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
protected internal MenuArgs(AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint>? eventInterfaces)
|
||||
{
|
||||
this.AddonName = addon != null ? MemoryHelper.ReadString((nint)addon->Name, 32) : null;
|
||||
this.AddonPtr = (nint)addon;
|
||||
this.AgentPtr = (nint)agent;
|
||||
this.MenuType = type;
|
||||
this.eventInterfaces = eventInterfaces;
|
||||
this.Target = type switch
|
||||
{
|
||||
ContextMenuType.Default => new MenuTargetDefault((AgentContext*)agent),
|
||||
ContextMenuType.Inventory => new MenuTargetInventory((AgentInventoryContext*)agent),
|
||||
_ => throw new ArgumentException("Invalid context menu type", nameof(type)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon that opened the context menu.
|
||||
/// </summary>
|
||||
public string? AddonName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memory pointer of the addon that opened the context menu.
|
||||
/// </summary>
|
||||
public nint AddonPtr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memory pointer of the agent that opened the context menu.
|
||||
/// </summary>
|
||||
public nint AgentPtr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the context menu.
|
||||
/// </summary>
|
||||
public ContextMenuType MenuType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target info of the context menu. The actual type depends on <see cref="MenuType"/>.
|
||||
/// <see cref="ContextMenuType.Default"/> signifies a <see cref="MenuTargetDefault"/>.
|
||||
/// <see cref="ContextMenuType.Inventory"/> signifies a <see cref="MenuTargetInventory"/>.
|
||||
/// </summary>
|
||||
public MenuTarget Target { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of AtkEventInterface pointers associated with the context menu.
|
||||
/// Only available with <see cref="ContextMenuType.Default"/>.
|
||||
/// Almost always an agent pointer. You can use this to find out what type of context menu it is.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the context menu is not a <see cref="ContextMenuType.Default"/>.</exception>
|
||||
public IReadOnlySet<nint> EventInterfaces =>
|
||||
this.MenuType != ContextMenuType.Default ?
|
||||
this.eventInterfaces :
|
||||
throw new InvalidOperationException("Not a default context menu");
|
||||
}
|
||||
91
Dalamud/Game/Gui/ContextMenu/MenuItem.cs
Normal file
91
Dalamud/Game/Gui/ContextMenu/MenuItem.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// A menu item that can be added to a context menu.
|
||||
/// </summary>
|
||||
public sealed record MenuItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the menu item.
|
||||
/// </summary>
|
||||
public SeString Name { get; set; } = SeString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the prefix attached to the beginning of <see cref="Name"/>.
|
||||
/// </summary>
|
||||
public SeIconChar? Prefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the character to prefix the <see cref="Name"/> with. Will be converted into a fancy boxed letter icon. Must be an uppercase letter.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException"><paramref name="value"/> must be an uppercase letter.</exception>
|
||||
public char? PrefixChar
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value is { } prefix)
|
||||
{
|
||||
if (!char.IsAsciiLetterUpper(prefix))
|
||||
throw new ArgumentException("Prefix must be an uppercase letter", nameof(value));
|
||||
|
||||
this.Prefix = SeIconChar.BoxedLetterA + prefix - 'A';
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Prefix = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the <see cref="Prefix"/>. Specifies a <see cref="UIColor"/> row id.
|
||||
/// </summary>
|
||||
public ushort PrefixColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback to be invoked when the menu item is clicked.
|
||||
/// </summary>
|
||||
public Action<MenuItemClickedArgs>? OnClicked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority (or order) with which the menu item should be displayed in descending order.
|
||||
/// Priorities below 0 will be displayed above the native menu items.
|
||||
/// Other priorities will be displayed below the native menu items.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is enabled.
|
||||
/// Disabled items will be faded and cannot be clicked on.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is a submenu.
|
||||
/// This value is purely visual. Submenu items will have an arrow to its right.
|
||||
/// </summary>
|
||||
public bool IsSubmenu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is a return item.
|
||||
/// This value is purely visual. Return items will have a back arrow to its left.
|
||||
/// If both <see cref="IsSubmenu"/> and <see cref="IsReturn"/> are true, the return arrow will take precedence.
|
||||
/// </summary>
|
||||
public bool IsReturn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name with the given prefix.
|
||||
/// </summary>
|
||||
internal SeString PrefixedName =>
|
||||
this.Prefix is { } prefix
|
||||
? new SeStringBuilder()
|
||||
.AddUiForeground($"{prefix.ToIconString()} ", this.PrefixColor)
|
||||
.Append(this.Name)
|
||||
.Build()
|
||||
: this.Name;
|
||||
}
|
||||
44
Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs
Normal file
44
Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Callback args used when a menu item is clicked.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuItemClickedArgs : MenuArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuItemClickedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="openSubmenu">Callback for opening a submenu.</param>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
internal MenuItemClickedArgs(Action<SeString?, IReadOnlyList<MenuItem>> openSubmenu, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
|
||||
: base(addon, agent, type, eventInterfaces)
|
||||
{
|
||||
this.OnOpenSubmenu = openSubmenu;
|
||||
}
|
||||
|
||||
private Action<SeString?, IReadOnlyList<MenuItem>> OnOpenSubmenu { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens a submenu with the given name and items.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the submenu, displayed at the top.</param>
|
||||
/// <param name="items">The items to display in the submenu.</param>
|
||||
public void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> items) =>
|
||||
this.OnOpenSubmenu(name, items);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a submenu with the given items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to display in the submenu.</param>
|
||||
public void OpenSubmenu(IReadOnlyList<MenuItem> items) =>
|
||||
this.OnOpenSubmenu(null, items);
|
||||
}
|
||||
34
Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs
Normal file
34
Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Callback args used when a menu item is opened.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuOpenedArgs : MenuArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuOpenedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addMenuItem">Callback for adding a custom menu item.</param>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
internal MenuOpenedArgs(Action<MenuItem> addMenuItem, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
|
||||
: base(addon, agent, type, eventInterfaces)
|
||||
{
|
||||
this.OnAddMenuItem = addMenuItem;
|
||||
}
|
||||
|
||||
private Action<MenuItem> OnAddMenuItem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom menu item to the context menu.
|
||||
/// </summary>
|
||||
/// <param name="item">The menu item to add.</param>
|
||||
public void AddMenuItem(MenuItem item) =>
|
||||
this.OnAddMenuItem(item);
|
||||
}
|
||||
9
Dalamud/Game/Gui/ContextMenu/MenuTarget.cs
Normal file
9
Dalamud/Game/Gui/ContextMenu/MenuTarget.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="MenuArgs"/> contexts.
|
||||
/// Discriminated based on <see cref="ContextMenuType"/>.
|
||||
/// </summary>
|
||||
public abstract class MenuTarget
|
||||
{
|
||||
}
|
||||
67
Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs
Normal file
67
Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Resolvers;
|
||||
using Dalamud.Game.Network.Structures.InfoProxy;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Target information on a default context menu.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuTargetDefault : MenuTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuTargetDefault"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The agent associated with the context menu.</param>
|
||||
internal MenuTargetDefault(AgentContext* context)
|
||||
{
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the target.
|
||||
/// </summary>
|
||||
public string TargetName => this.Context->TargetName.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object id of the target.
|
||||
/// </summary>
|
||||
public ulong TargetObjectId => this.Context->TargetObjectId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target object.
|
||||
/// </summary>
|
||||
public GameObject? TargetObject => Service<ObjectTable>.Get().SearchById(this.TargetObjectId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content id of the target.
|
||||
/// </summary>
|
||||
public ulong TargetContentId => this.Context->TargetContentId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home world id of the target.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members.
|
||||
/// Just because this is <see langword="null"/> doesn't mean the target isn't a character.
|
||||
/// </summary>
|
||||
public CharacterData? TargetCharacter
|
||||
{
|
||||
get
|
||||
{
|
||||
var target = this.Context->CurrentContextMenuTarget;
|
||||
if (target != null)
|
||||
return new(target);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentContext* Context { get; }
|
||||
}
|
||||
36
Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs
Normal file
36
Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using Dalamud.Game.Inventory;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Target information on an inventory context menu.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuTargetInventory : MenuTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuTargetInventory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The agent associated with the context menu.</param>
|
||||
internal MenuTargetInventory(AgentInventoryContext* context)
|
||||
{
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target item.
|
||||
/// </summary>
|
||||
public GameInventoryItem? TargetItem
|
||||
{
|
||||
get
|
||||
{
|
||||
var target = this.Context->TargetInventorySlot;
|
||||
if (target != null)
|
||||
return new(*target);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentInventoryContext* Context { get; }
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
|
||||
namespace Dalamud.Game.Inventory;
|
||||
|
|
@ -103,8 +106,10 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
|||
/// <summary>
|
||||
/// Gets the array of materia grades.
|
||||
/// </summary>
|
||||
// TODO: Replace with MateriaGradeBytes
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public ReadOnlySpan<ushort> MateriaGrade =>
|
||||
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||
this.MateriaGradeBytes.ToArray().Select(g => (ushort)g).ToArray().AsSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of native inventory item in the game.<br />
|
||||
|
|
@ -146,6 +151,9 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
|||
/// </summary>
|
||||
internal ulong CrafterContentId => this.InternalItem.CrafterContentID;
|
||||
|
||||
private ReadOnlySpan<byte> MateriaGradeBytes =>
|
||||
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||
|
||||
public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r);
|
||||
|
||||
public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Dalamud.Game.Gui;
|
|||
using Dalamud.Game.Network.Internal.MarketBoardUploaders;
|
||||
using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis;
|
||||
using Dalamud.Game.Network.Structures;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Networking.Http;
|
||||
using Dalamud.Utility;
|
||||
|
|
@ -268,8 +269,8 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType
|
|||
return result;
|
||||
}
|
||||
|
||||
var cfcName = cfCondition.Name.ToString();
|
||||
if (cfcName.IsNullOrEmpty())
|
||||
var cfcName = cfCondition.Name.ToDalamudString();
|
||||
if (cfcName.Payloads.Count == 0)
|
||||
{
|
||||
cfcName = "Duty Roulette";
|
||||
cfCondition.Image = 112324;
|
||||
|
|
@ -279,7 +280,10 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType
|
|||
{
|
||||
if (this.configuration.DutyFinderChatMessage)
|
||||
{
|
||||
Service<ChatGui>.GetNullable()?.Print($"Duty pop: {cfcName}");
|
||||
var b = new SeStringBuilder();
|
||||
b.Append("Duty pop: ");
|
||||
b.Append(cfcName);
|
||||
Service<ChatGui>.GetNullable()?.Print(b.Build());
|
||||
}
|
||||
|
||||
this.CfPop.InvokeSafely(cfCondition);
|
||||
|
|
|
|||
197
Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs
Normal file
197
Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.ClientState.Resolvers;
|
||||
using Dalamud.Memory;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Network.Structures.InfoProxy;
|
||||
|
||||
/// <summary>
|
||||
/// Dalamud wrapper around a client structs <see cref="InfoProxyCommonList.CharacterData"/>.
|
||||
/// </summary>
|
||||
public unsafe class CharacterData
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CharacterData"/> class.
|
||||
/// </summary>
|
||||
/// <param name="data">Character data to wrap.</param>
|
||||
internal CharacterData(InfoProxyCommonList.CharacterData* data)
|
||||
{
|
||||
this.Address = (nint)data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the <see cref="InfoProxyCommonList.CharacterData"/> in memory.
|
||||
/// </summary>
|
||||
public nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content id of the character.
|
||||
/// </summary>
|
||||
public ulong ContentId => this.Struct->ContentId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status mask of the character.
|
||||
/// </summary>
|
||||
public ulong StatusMask => (ulong)this.Struct->State;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the applicable statues of the character.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ExcelResolver<OnlineStatus>> Statuses
|
||||
{
|
||||
get
|
||||
{
|
||||
var statuses = new List<ExcelResolver<OnlineStatus>>();
|
||||
for (var i = 0; i < 64; i++)
|
||||
{
|
||||
if ((this.StatusMask & (1UL << i)) != 0)
|
||||
statuses.Add(new((uint)i));
|
||||
}
|
||||
|
||||
return statuses;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display group of the character.
|
||||
/// </summary>
|
||||
public DisplayGroup DisplayGroup => (DisplayGroup)this.Struct->Group;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the character's home world is different from the current world.
|
||||
/// </summary>
|
||||
public bool IsFromOtherServer => this.Struct->IsOtherServer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sort order of the character.
|
||||
/// </summary>
|
||||
public byte Sort => this.Struct->Sort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current world of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> CurrentWorld => new(this.Struct->CurrentWorld);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home world of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> HomeWorld => new(this.Struct->HomeWorld);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the location of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<TerritoryType> Location => new(this.Struct->Location);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the grand company of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<GrandCompany> GrandCompany => new((uint)this.Struct->GrandCompany);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary client language of the character.
|
||||
/// </summary>
|
||||
public ClientLanguage ClientLanguage => (ClientLanguage)this.Struct->ClientLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported language mask of the character.
|
||||
/// </summary>
|
||||
public byte LanguageMask => (byte)this.Struct->Languages;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported languages the character supports.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ClientLanguage> Languages
|
||||
{
|
||||
get
|
||||
{
|
||||
var languages = new List<ClientLanguage>();
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
if ((this.LanguageMask & (1 << i)) != 0)
|
||||
languages.Add((ClientLanguage)i);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gender of the character.
|
||||
/// </summary>
|
||||
public byte Gender => this.Struct->Sex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the job of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->Job);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the character.
|
||||
/// </summary>
|
||||
public string Name => MemoryHelper.ReadString((nint)this.Struct->Name, 32);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free company tag of the character.
|
||||
/// </summary>
|
||||
public string FCTag => MemoryHelper.ReadString((nint)this.Struct->Name, 6);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying <see cref="InfoProxyCommonList.CharacterData"/> struct.
|
||||
/// </summary>
|
||||
internal InfoProxyCommonList.CharacterData* Struct => (InfoProxyCommonList.CharacterData*)this.Address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display group of a character. Used for friends.
|
||||
/// </summary>
|
||||
public enum DisplayGroup : sbyte
|
||||
{
|
||||
/// <summary>
|
||||
/// All display groups.
|
||||
/// </summary>
|
||||
All = -1,
|
||||
|
||||
/// <summary>
|
||||
/// No display group.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Star display group.
|
||||
/// </summary>
|
||||
Star,
|
||||
|
||||
/// <summary>
|
||||
/// Circle display group.
|
||||
/// </summary>
|
||||
Circle,
|
||||
|
||||
/// <summary>
|
||||
/// Triangle display group.
|
||||
/// </summary>
|
||||
Triangle,
|
||||
|
||||
/// <summary>
|
||||
/// Diamond display group.
|
||||
/// </summary>
|
||||
Diamond,
|
||||
|
||||
/// <summary>
|
||||
/// Heart display group.
|
||||
/// </summary>
|
||||
Heart,
|
||||
|
||||
/// <summary>
|
||||
/// Spade display group.
|
||||
/// </summary>
|
||||
Spade,
|
||||
|
||||
/// <summary>
|
||||
/// Club display group.
|
||||
/// </summary>
|
||||
Club,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue