mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-13 12:14:16 +01:00
Make DtrBar more threadsafe (#1978)
* Changed DtrBar to use ReaderWriterLockSlim so that there exists only one storage of entries, preventing possible desync. * DtrBarEntry will now hold a reference to the LocalPlugin that created the entry, so that DtrBarPluginScoped can defer plugin related handling to the main service. * Marked DtrBarEntry class itself to be turned internal in API 11. * Made IDtrBar.Entries return an immutable copy of underlying list of DtrBar entries, that will be freshly created whenever the list changes.
This commit is contained in:
parent
a7ab3b9def
commit
c25f13261d
10 changed files with 385 additions and 207 deletions
|
|
@ -150,7 +150,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
};
|
||||
}
|
||||
|
||||
[Api10ToDo("Use ThreadSafety.AssertMainThread() instead of this.")]
|
||||
[Api11ToDo("Use ThreadSafety.AssertMainThread() instead of this.")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool WarnMultithreadedUsage()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.Addon.Events;
|
||||
|
|
@ -10,6 +11,7 @@ using Dalamud.Game.Text.SeStringHandling;
|
|||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics;
|
||||
|
|
@ -48,11 +50,13 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
private readonly AddonLifecycleEventListener dtrPostRequestedUpdateListener;
|
||||
private readonly AddonLifecycleEventListener dtrPreFinalizeListener;
|
||||
|
||||
private readonly ConcurrentBag<DtrBarEntry> newEntries = new();
|
||||
private readonly List<DtrBarEntry> entries = new();
|
||||
private readonly ReaderWriterLockSlim entriesLock = new();
|
||||
private readonly List<DtrBarEntry> entries = [];
|
||||
|
||||
private readonly Dictionary<uint, List<IAddonEventHandle>> eventHandles = new();
|
||||
|
||||
private ImmutableList<IReadOnlyDtrBarEntry>? entriesReadOnlyCopy;
|
||||
|
||||
private Utf8String* emptyString;
|
||||
|
||||
private uint runningNodeIds = BaseNodeId;
|
||||
|
|
@ -71,52 +75,108 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
|
||||
this.framework.Update += this.Update;
|
||||
|
||||
this.configuration.DtrOrder ??= new List<string>();
|
||||
this.configuration.DtrIgnore ??= new List<string>();
|
||||
this.configuration.DtrOrder ??= [];
|
||||
this.configuration.DtrIgnore ??= [];
|
||||
this.configuration.QueueSave();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event type fired each time a DtrEntry was removed.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the bar entry.</param>
|
||||
internal delegate void DtrEntryRemovedDelegate(string title);
|
||||
|
||||
/// <summary>
|
||||
/// Event fired each time a DtrEntry was removed.
|
||||
/// </summary>
|
||||
internal event DtrEntryRemovedDelegate? DtrEntryRemoved;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.entries;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDtrBarEntry Get(string title, SeString? text = null)
|
||||
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries
|
||||
{
|
||||
if (this.entries.Any(x => x.Title == title) || this.newEntries.Any(x => x.Title == title))
|
||||
throw new ArgumentException("An entry with the same title already exists.");
|
||||
|
||||
var entry = new DtrBarEntry(this.configuration, title, null);
|
||||
entry.Text = text;
|
||||
|
||||
// Add the entry to the end of the order list, if it's not there already.
|
||||
if (!this.configuration.DtrOrder!.Contains(title))
|
||||
this.configuration.DtrOrder!.Add(title);
|
||||
|
||||
this.newEntries.Add(entry);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(string title)
|
||||
{
|
||||
if (this.entries.FirstOrDefault(entry => entry.Title == title) is { } dtrBarEntry)
|
||||
get
|
||||
{
|
||||
dtrBarEntry.Remove();
|
||||
var erc = this.entriesReadOnlyCopy;
|
||||
if (erc is null)
|
||||
{
|
||||
this.entriesLock.EnterReadLock();
|
||||
this.entriesReadOnlyCopy = erc = [..this.entries];
|
||||
this.entriesLock.ExitReadLock();
|
||||
}
|
||||
|
||||
return erc;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a DTR bar entry.
|
||||
/// This allows you to add your own text, and users to sort it.
|
||||
/// </summary>
|
||||
/// <param name="plugin">Plugin that owns the DTR bar, or <c>null</c> if owned by Dalamud.</param>
|
||||
/// <param name="title">A user-friendly name for sorting.</param>
|
||||
/// <param name="text">The text the entry shows.</param>
|
||||
/// <returns>The entry object used to update, hide and remove the entry.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
|
||||
public IDtrBarEntry Get(LocalPlugin? plugin, string title, SeString? text = null)
|
||||
{
|
||||
this.entriesLock.EnterUpgradeableReadLock();
|
||||
|
||||
foreach (var existingEntry in this.entries)
|
||||
{
|
||||
if (existingEntry.Title == title)
|
||||
{
|
||||
existingEntry.ShouldBeRemoved = false;
|
||||
this.entriesLock.ExitUpgradeableReadLock();
|
||||
if (plugin == existingEntry.OwnerPlugin)
|
||||
return existingEntry;
|
||||
throw new ArgumentException("An entry with the same title already exists.");
|
||||
}
|
||||
}
|
||||
|
||||
this.entriesLock.EnterWriteLock();
|
||||
var entry = new DtrBarEntry(this.configuration, title, null) { Text = text, OwnerPlugin = plugin };
|
||||
this.entries.Add(entry);
|
||||
|
||||
// Add the entry to the end of the order list, if it's not there already.
|
||||
var dtrOrder = this.configuration.DtrOrder ??= [];
|
||||
if (!dtrOrder.Contains(entry.Title))
|
||||
dtrOrder.Add(entry.Title);
|
||||
this.ApplySortUnsafe(dtrOrder);
|
||||
|
||||
this.entriesReadOnlyCopy = null;
|
||||
this.entriesLock.ExitWriteLock();
|
||||
|
||||
this.entriesLock.ExitUpgradeableReadLock();
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDtrBarEntry Get(string title, SeString? text = null) => this.Get(null, title, text);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a DTR bar entry from the system.
|
||||
/// </summary>
|
||||
/// <param name="plugin">Plugin that owns the DTR bar, or <c>null</c> if owned by Dalamud.</param>
|
||||
/// <param name="title">Title of the entry to remove, or <c>null</c> to remove all entries under the plugin.</param>
|
||||
/// <remarks>Remove operation is not immediate. If you try to add right after removing, the operation may fail.
|
||||
/// </remarks>
|
||||
public void Remove(LocalPlugin? plugin, string? title)
|
||||
{
|
||||
this.entriesLock.EnterUpgradeableReadLock();
|
||||
|
||||
foreach (var entry in this.entries)
|
||||
{
|
||||
if ((title is null || entry.Title == title) && (plugin is null || entry.OwnerPlugin == plugin))
|
||||
{
|
||||
if (!entry.Added)
|
||||
{
|
||||
this.entriesLock.EnterWriteLock();
|
||||
this.RemoveEntry(entry);
|
||||
this.entries.Remove(entry);
|
||||
this.entriesReadOnlyCopy = null;
|
||||
this.entriesLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
entry.Remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.entriesLock.ExitUpgradeableReadLock();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(string title) => this.Remove(null, title);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
|
|
@ -124,10 +184,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener);
|
||||
this.addonLifecycle.UnregisterListener(this.dtrPreFinalizeListener);
|
||||
|
||||
foreach (var entry in this.entries)
|
||||
this.RemoveEntry(entry);
|
||||
this.framework.RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
this.entriesLock.EnterWriteLock();
|
||||
foreach (var entry in this.entries)
|
||||
this.RemoveEntry(entry);
|
||||
this.entries.Clear();
|
||||
this.entriesReadOnlyCopy = null;
|
||||
this.entriesLock.ExitWriteLock();
|
||||
}).Wait();
|
||||
|
||||
this.entries.Clear();
|
||||
this.framework.Update -= this.Update;
|
||||
|
||||
if (this.emptyString != null)
|
||||
|
|
@ -137,23 +204,6 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove nodes marked as "should be removed" from the bar.
|
||||
/// </summary>
|
||||
internal void HandleRemovedNodes()
|
||||
{
|
||||
foreach (var data in this.entries)
|
||||
{
|
||||
if (data.ShouldBeRemoved)
|
||||
{
|
||||
this.RemoveEntry(data);
|
||||
this.DtrEntryRemoved?.Invoke(data.Title);
|
||||
}
|
||||
}
|
||||
|
||||
this.entries.RemoveAll(d => d.ShouldBeRemoved);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove native resources for the specified entry.
|
||||
/// </summary>
|
||||
|
|
@ -174,7 +224,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
/// </summary>
|
||||
/// <param name="title">The title to check for.</param>
|
||||
/// <returns>Whether or not an entry with that title is registered.</returns>
|
||||
internal bool HasEntry(string title) => this.entries.Any(x => x.Title == title);
|
||||
internal bool HasEntry(string title)
|
||||
{
|
||||
var found = false;
|
||||
|
||||
this.entriesLock.EnterReadLock();
|
||||
for (var i = 0; i < this.entries.Count && !found; i++)
|
||||
found = this.entries[i].Title == title;
|
||||
this.entriesLock.ExitReadLock();
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dirty the DTR bar entry with the specified title.
|
||||
|
|
@ -183,24 +243,37 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
/// <returns>Whether the entry was found.</returns>
|
||||
internal bool MakeDirty(string title)
|
||||
{
|
||||
var entry = this.entries.FirstOrDefault(x => x.Title == title);
|
||||
if (entry == null)
|
||||
return false;
|
||||
var found = false;
|
||||
|
||||
entry.Dirty = true;
|
||||
return true;
|
||||
this.entriesLock.EnterReadLock();
|
||||
for (var i = 0; i < this.entries.Count && !found; i++)
|
||||
{
|
||||
found = this.entries[i].Title == title;
|
||||
if (found)
|
||||
this.entries[i].Dirty = true;
|
||||
}
|
||||
|
||||
this.entriesLock.ExitReadLock();
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reapply the DTR entry ordering from <see cref="DalamudConfiguration"/>.
|
||||
/// </summary>
|
||||
internal void ApplySort()
|
||||
{
|
||||
this.entriesLock.EnterWriteLock();
|
||||
this.ApplySortUnsafe(this.configuration.DtrOrder ??= []);
|
||||
this.entriesLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
private void ApplySortUnsafe(List<string> dtrOrder)
|
||||
{
|
||||
// Sort the current entry list, based on the order in the configuration.
|
||||
var positions = this.configuration
|
||||
.DtrOrder!
|
||||
.Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry)))
|
||||
.ToDictionary(x => x.entry, x => x.index);
|
||||
var positions = dtrOrder
|
||||
.Select(entry => (entry, index: dtrOrder.IndexOf(entry)))
|
||||
.ToDictionary(x => x.entry, x => x.index);
|
||||
|
||||
this.entries.Sort((x, y) =>
|
||||
{
|
||||
|
|
@ -208,15 +281,13 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
var yPos = positions.TryGetValue(y.Title, out var yIndex) ? yIndex : int.MaxValue;
|
||||
return xPos.CompareTo(yPos);
|
||||
});
|
||||
this.entriesReadOnlyCopy = null;
|
||||
}
|
||||
|
||||
private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer();
|
||||
|
||||
private void Update(IFramework unused)
|
||||
{
|
||||
this.HandleRemovedNodes();
|
||||
this.HandleAddedNodes();
|
||||
|
||||
var dtr = this.GetDtr();
|
||||
if (dtr == null || dtr->RootNode == null || dtr->RootNode->ChildNode == null) return;
|
||||
|
||||
|
|
@ -236,14 +307,27 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
|
||||
var runningXPos = this.entryStartPos;
|
||||
|
||||
foreach (var data in this.entries)
|
||||
this.entriesLock.EnterUpgradeableReadLock();
|
||||
for (var i = 0; i < this.entries.Count; i++)
|
||||
{
|
||||
if (!data.Added)
|
||||
var data = this.entries[i];
|
||||
if (data.ShouldBeRemoved)
|
||||
{
|
||||
data.Added = this.AddNode(data.TextNode);
|
||||
data.Dirty = true;
|
||||
this.entriesLock.EnterWriteLock();
|
||||
this.entries.RemoveAt(i);
|
||||
this.RemoveEntry(data);
|
||||
this.entriesReadOnlyCopy = null;
|
||||
this.entriesLock.ExitWriteLock();
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!data.Added)
|
||||
data.Added = this.AddNode(data);
|
||||
|
||||
if (!data.Added || data.TextNode is null) // TextNode check is unnecessary, but just in case.
|
||||
continue;
|
||||
|
||||
var isHide = !data.Shown || data.UserHidden;
|
||||
var node = data.TextNode;
|
||||
var nodeHidden = !node->AtkResNode.IsVisible();
|
||||
|
|
@ -290,23 +374,10 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
|
||||
data.Dirty = false;
|
||||
}
|
||||
|
||||
this.entriesLock.ExitUpgradeableReadLock();
|
||||
}
|
||||
|
||||
private void HandleAddedNodes()
|
||||
{
|
||||
if (!this.newEntries.IsEmpty)
|
||||
{
|
||||
foreach (var newEntry in this.newEntries)
|
||||
{
|
||||
newEntry.TextNode = this.MakeNode(++this.runningNodeIds);
|
||||
this.entries.Add(newEntry);
|
||||
}
|
||||
|
||||
this.newEntries.Clear();
|
||||
this.ApplySort();
|
||||
}
|
||||
}
|
||||
|
||||
private void FixCollision(AddonEvent eventType, AddonArgs addonInfo)
|
||||
{
|
||||
var addon = (AtkUnitBase*)addonInfo.Addon;
|
||||
|
|
@ -316,7 +387,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
var additionalWidth = 0;
|
||||
AtkResNode* collisionNode = null;
|
||||
|
||||
foreach (var index in Enumerable.Range(0, addon->UldManager.NodeListCount))
|
||||
for (var index = 0; index < addon->UldManager.NodeListCount; index++)
|
||||
{
|
||||
var node = addon->UldManager.NodeList[index];
|
||||
if (node->IsVisible())
|
||||
|
|
@ -382,22 +453,20 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
private void RecreateNodes()
|
||||
{
|
||||
this.runningNodeIds = BaseNodeId;
|
||||
if (this.entries.Any())
|
||||
{
|
||||
this.eventHandles.Clear();
|
||||
}
|
||||
|
||||
this.entriesLock.EnterReadLock();
|
||||
this.eventHandles.Clear();
|
||||
foreach (var entry in this.entries)
|
||||
{
|
||||
entry.TextNode = this.MakeNode(++this.runningNodeIds);
|
||||
entry.Added = false;
|
||||
}
|
||||
this.entriesLock.ExitReadLock();
|
||||
}
|
||||
|
||||
private bool AddNode(AtkTextNode* node)
|
||||
private bool AddNode(DtrBarEntry data)
|
||||
{
|
||||
var dtr = this.GetDtr();
|
||||
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
|
||||
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null) return false;
|
||||
|
||||
var node = data.TextNode = this.MakeNode(++this.runningNodeIds);
|
||||
|
||||
this.eventHandles.TryAdd(node->AtkResNode.NodeId, new List<IAddonEventHandle>());
|
||||
this.eventHandles[node->AtkResNode.NodeId].AddRange(new List<IAddonEventHandle>
|
||||
|
|
@ -420,6 +489,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
dtr->UldManager.UpdateDrawNodeList();
|
||||
dtr->UpdateCollisionNodeList(false);
|
||||
Log.Debug("Updated node draw list");
|
||||
|
||||
data.Dirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -497,7 +568,15 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
var addon = (AtkUnitBase*)atkUnitBase;
|
||||
var node = (AtkResNode*)atkResNode;
|
||||
|
||||
if (this.entries.FirstOrDefault(entry => entry.TextNode == node) is not { } dtrBarEntry) return;
|
||||
DtrBarEntry? dtrBarEntry = null;
|
||||
this.entriesLock.EnterReadLock();
|
||||
foreach (var entry in this.entries)
|
||||
{
|
||||
if (entry.TextNode == node)
|
||||
dtrBarEntry = entry;
|
||||
}
|
||||
|
||||
this.entriesLock.ExitReadLock();
|
||||
|
||||
if (dtrBarEntry is { Tooltip: not null })
|
||||
{
|
||||
|
|
@ -541,58 +620,25 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IDtrBar>]
|
||||
#pragma warning restore SA1015
|
||||
internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
|
||||
internal sealed class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
|
||||
{
|
||||
private readonly LocalPlugin plugin;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DtrBar dtrBarService = Service<DtrBar>.Get();
|
||||
|
||||
private readonly Dictionary<string, IDtrBarEntry> pluginEntries = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DtrBarPluginScoped"/> class.
|
||||
/// </summary>
|
||||
internal DtrBarPluginScoped()
|
||||
{
|
||||
this.dtrBarService.DtrEntryRemoved += this.OnDtrEntryRemoved;
|
||||
}
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private DtrBarPluginScoped(LocalPlugin plugin) => this.plugin = plugin;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.dtrBarService.Entries;
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.dtrBarService.DtrEntryRemoved -= this.OnDtrEntryRemoved;
|
||||
|
||||
foreach (var entry in this.pluginEntries)
|
||||
{
|
||||
entry.Value.Remove();
|
||||
}
|
||||
|
||||
this.pluginEntries.Clear();
|
||||
}
|
||||
void IInternalDisposableService.DisposeService() => this.dtrBarService.Remove(this.plugin, null);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDtrBarEntry Get(string title, SeString? text = null)
|
||||
{
|
||||
// If we already have a known entry for this plugin, return it.
|
||||
if (this.pluginEntries.TryGetValue(title, out var existingEntry)) return existingEntry;
|
||||
public IDtrBarEntry Get(string title, SeString? text = null) => this.dtrBarService.Get(this.plugin, title, text);
|
||||
|
||||
return this.pluginEntries[title] = this.dtrBarService.Get(title, text);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(string title)
|
||||
{
|
||||
if (this.pluginEntries.TryGetValue(title, out var existingEntry))
|
||||
{
|
||||
existingEntry.Remove();
|
||||
this.pluginEntries.Remove(title);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDtrEntryRemoved(string title)
|
||||
{
|
||||
this.pluginEntries.Remove(title);
|
||||
}
|
||||
public void Remove(string title) => this.dtrBarService.Remove(this.plugin, title);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System.Linq;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
|
|
@ -86,6 +85,7 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
|
|||
/// <summary>
|
||||
/// Class representing an entry in the server info bar.
|
||||
/// </summary>
|
||||
[Api11ToDo(Api11ToDoAttribute.MakeInternal)]
|
||||
public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
|
||||
{
|
||||
private readonly DalamudConfiguration configuration;
|
||||
|
|
@ -146,7 +146,7 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Api10ToDo("Maybe make this config scoped to internalname?")]
|
||||
[Api11ToDo("Maybe make this config scoped to internalname?")]
|
||||
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -160,9 +160,9 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
|
|||
internal Utf8String* Storage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this entry should be removed.
|
||||
/// Gets or sets a value indicating whether this entry should be removed.
|
||||
/// </summary>
|
||||
internal bool ShouldBeRemoved { get; private set; }
|
||||
internal bool ShouldBeRemoved { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this entry is dirty.
|
||||
|
|
@ -174,6 +174,11 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
|
|||
/// </summary>
|
||||
internal bool Added { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the plugin that owns this entry.
|
||||
/// </summary>
|
||||
internal LocalPlugin? OwnerPlugin { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TriggerClickAction()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
using Dalamud.Configuration.Internal;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Gui.Dtr;
|
||||
|
||||
using ImGuiNET;
|
||||
|
|
@ -8,17 +12,20 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
/// <summary>
|
||||
/// Widget for displaying dtr test.
|
||||
/// </summary>
|
||||
internal class DtrBarWidget : IDataWindowWidget
|
||||
internal class DtrBarWidget : IDataWindowWidget, IDisposable
|
||||
{
|
||||
private IDtrBarEntry? dtrTest1;
|
||||
private IDtrBarEntry? dtrTest2;
|
||||
private IDtrBarEntry? dtrTest3;
|
||||
|
||||
|
||||
private Thread? loadTestThread;
|
||||
private CancellationTokenSource? loadTestThreadCt;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; } = { "dtr", "dtrbar" };
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "DTR Bar";
|
||||
public string DisplayName { get; init; } = "DTR Bar";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
|
@ -26,31 +33,145 @@ internal class DtrBarWidget : IDataWindowWidget
|
|||
/// <inheritdoc/>
|
||||
public void Load()
|
||||
{
|
||||
this.ClearState();
|
||||
this.Ready = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this.ClearState();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Draw()
|
||||
{
|
||||
if (this.loadTestThread?.IsAlive is not true)
|
||||
{
|
||||
if (ImGui.Button("Do multithreaded add/remove operation"))
|
||||
{
|
||||
var ct = this.loadTestThreadCt = new();
|
||||
var dbar = Service<DtrBar>.Get();
|
||||
var fw = Service<Framework>.Get();
|
||||
var rng = new Random();
|
||||
this.loadTestThread = new(
|
||||
() =>
|
||||
{
|
||||
var threads = Enumerable
|
||||
.Range(0, Environment.ProcessorCount)
|
||||
.Select(
|
||||
i => new Thread(
|
||||
(i % 4) switch
|
||||
{
|
||||
0 => () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var n = $"DtrBarWidgetTest{rng.NextInt64(8)}";
|
||||
dbar.Get(n, n[^5..]);
|
||||
fw.DelayTicks(1, ct.Token).Wait(ct.Token);
|
||||
ct.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
1 => () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
dbar.Remove($"DtrBarWidgetTest{rng.NextInt64(8)}");
|
||||
fw.DelayTicks(1, ct.Token).Wait(ct.Token);
|
||||
ct.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
2 => () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var n = $"DtrBarWidgetTest{rng.NextInt64(8)}_";
|
||||
dbar.Get(n, n[^6..]);
|
||||
ct.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
_ => () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
dbar.Remove($"DtrBarWidgetTest{rng.NextInt64(8)}_");
|
||||
ct.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
}))
|
||||
.ToArray();
|
||||
foreach (var t in threads) t.Start();
|
||||
foreach (var t in threads) t.Join();
|
||||
for (var i = 0; i < 8; i++) dbar.Remove($"DtrBarWidgetTest{i % 8}");
|
||||
for (var i = 0; i < 8; i++) dbar.Remove($"DtrBarWidgetTest{i % 8}_");
|
||||
});
|
||||
this.loadTestThread.Start();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ImGui.Button("Stop multithreaded add/remove operation"))
|
||||
this.ClearState();
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
this.DrawDtrTestEntry(ref this.dtrTest1, "DTR Test #1");
|
||||
|
||||
ImGui.Separator();
|
||||
this.DrawDtrTestEntry(ref this.dtrTest2, "DTR Test #2");
|
||||
|
||||
ImGui.Separator();
|
||||
this.DrawDtrTestEntry(ref this.dtrTest3, "DTR Test #3");
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.Text("IDtrBar.Entries:");
|
||||
foreach (var e in Service<DtrBar>.Get().Entries)
|
||||
ImGui.Text(e.Title);
|
||||
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
if (configuration.DtrOrder != null)
|
||||
{
|
||||
ImGui.Separator();
|
||||
|
||||
ImGui.Text("DtrOrder:");
|
||||
foreach (var order in configuration.DtrOrder)
|
||||
{
|
||||
ImGui.Text(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void ClearState()
|
||||
{
|
||||
this.loadTestThreadCt?.Cancel();
|
||||
this.loadTestThread?.Join();
|
||||
this.loadTestThread = null;
|
||||
this.loadTestThreadCt = null;
|
||||
}
|
||||
|
||||
private void DrawDtrTestEntry(ref IDtrBarEntry? entry, string title)
|
||||
{
|
||||
var dtrBar = Service<DtrBar>.Get();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using CheapLoc;
|
||||
using Dalamud.Configuration.Internal;
|
||||
|
|
@ -45,6 +46,10 @@ public class SettingsTabDtr : SettingsTab
|
|||
}
|
||||
|
||||
var isOrderChange = false;
|
||||
Span<Vector2> upButtonCenters = stackalloc Vector2[order.Count];
|
||||
Span<Vector2> downButtonCenters = stackalloc Vector2[order.Count];
|
||||
scoped Span<Vector2> moveMouseTo = default;
|
||||
var moveMouseToIndex = -1;
|
||||
for (var i = 0; i < order.Count; i++)
|
||||
{
|
||||
var title = order[i];
|
||||
|
|
@ -65,9 +70,13 @@ public class SettingsTabDtr : SettingsTab
|
|||
{
|
||||
(order[i], order[i - 1]) = (order[i - 1], order[i]);
|
||||
isOrderChange = true;
|
||||
moveMouseToIndex = i - 1;
|
||||
moveMouseTo = upButtonCenters;
|
||||
}
|
||||
}
|
||||
|
||||
upButtonCenters[i] = (ImGui.GetItemRectMin() + ImGui.GetItemRectMax()) / 2;
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
var arrowDownText = $"{FontAwesomeIcon.ArrowDown.ToIconString()}##{title}";
|
||||
|
|
@ -81,9 +90,13 @@ public class SettingsTabDtr : SettingsTab
|
|||
{
|
||||
(order[i], order[i + 1]) = (order[i + 1], order[i]);
|
||||
isOrderChange = true;
|
||||
moveMouseToIndex = i + 1;
|
||||
moveMouseTo = downButtonCenters;
|
||||
}
|
||||
}
|
||||
|
||||
downButtonCenters[i] = (ImGui.GetItemRectMin() + ImGui.GetItemRectMax()) / 2;
|
||||
|
||||
ImGui.PopFont();
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
@ -107,6 +120,12 @@ public class SettingsTabDtr : SettingsTab
|
|||
// }
|
||||
}
|
||||
|
||||
if (moveMouseToIndex >= 0 && moveMouseToIndex < moveMouseTo.Length)
|
||||
{
|
||||
ImGui.GetIO().WantSetMousePos = true;
|
||||
ImGui.GetIO().MousePos = moveMouseTo[moveMouseToIndex];
|
||||
}
|
||||
|
||||
configuration.DtrOrder = order.Concat(orderLeft).ToList();
|
||||
configuration.DtrIgnore = ignore.Concat(ignoreLeft).ToList();
|
||||
|
||||
|
|
|
|||
|
|
@ -15,13 +15,11 @@ using Dalamud.Configuration;
|
|||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Game.Gui.Dtr;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Networking.Http;
|
||||
|
|
@ -1123,10 +1121,6 @@ internal class PluginManager : IInternalDisposableService
|
|||
return updateStatus;
|
||||
}
|
||||
|
||||
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
|
||||
var dtr = Service<DtrBar>.Get();
|
||||
dtr.HandleRemovedNodes();
|
||||
|
||||
try
|
||||
{
|
||||
await this.InstallPluginInternalAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update, updateStream, workingPluginId);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ using System.Threading.Tasks;
|
|||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Game.Gui.Dtr;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
|
@ -15,7 +13,6 @@ using Dalamud.Plugin.Internal.Exceptions;
|
|||
using Dalamud.Plugin.Internal.Loader;
|
||||
using Dalamud.Plugin.Internal.Profiles;
|
||||
using Dalamud.Plugin.Internal.Types.Manifest;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
||||
|
|
@ -540,9 +537,6 @@ internal class LocalPlugin : IDisposable
|
|||
}
|
||||
finally
|
||||
{
|
||||
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
|
||||
Service<DtrBar>.GetNullable()?.HandleRemovedNodes();
|
||||
|
||||
this.pluginLoadStateLock.Release();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
using Dalamud.Game.Gui.Dtr;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
|
|
@ -12,10 +11,13 @@ namespace Dalamud.Plugin.Services;
|
|||
public interface IDtrBar
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a read-only list of all DTR bar entries.
|
||||
/// Gets a read-only copy of the list of all DTR bar entries.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries { get; }
|
||||
|
||||
/// <remarks>If the list changes due to changes in order or insertion/removal, then this property will return a
|
||||
/// completely new object on getter invocation. The returned object is safe to use from any thread, and will not
|
||||
/// change.</remarks>
|
||||
IReadOnlyList<IReadOnlyDtrBarEntry> Entries { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a DTR bar entry.
|
||||
/// This allows you to add your own text, and users to sort it.
|
||||
|
|
@ -24,11 +26,13 @@ public interface IDtrBar
|
|||
/// <param name="text">The text the entry shows.</param>
|
||||
/// <returns>The entry object used to update, hide and remove the entry.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
|
||||
public IDtrBarEntry Get(string title, SeString? text = null);
|
||||
IDtrBarEntry Get(string title, SeString? text = null);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a DTR bar entry from the system.
|
||||
/// </summary>
|
||||
/// <param name="title">Title of the entry to remove.</param>
|
||||
public void Remove(string title);
|
||||
/// <remarks>Remove operation is not guaranteed to be immediately effective. Calls to <see cref="Get"/> may result
|
||||
/// in an entry marked to be remove being revived and used again.</remarks>
|
||||
void Remove(string title);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for marking something to be changed for API 10, for ease of lookup.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
internal sealed class Api10ToDoAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks that this exists purely for making API 9 plugins work.
|
||||
/// </summary>
|
||||
public const string DeleteCompatBehavior = "Delete. This is for making API 9 plugins work.";
|
||||
|
||||
/// <summary>
|
||||
/// Marks that this should be moved to an another namespace.
|
||||
/// </summary>
|
||||
public const string MoveNamespace = "Move to another namespace.";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Api10ToDoAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="what">The explanation.</param>
|
||||
/// <param name="what2">The explanation 2.</param>
|
||||
public Api10ToDoAttribute(string what, string what2 = "")
|
||||
{
|
||||
_ = what;
|
||||
_ = what2;
|
||||
}
|
||||
}
|
||||
24
Dalamud/Utility/Api11ToDoAttribute.cs
Normal file
24
Dalamud/Utility/Api11ToDoAttribute.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for marking something to be changed for API 11, for ease of lookup.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
internal sealed class Api11ToDoAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks that this should be made internal.
|
||||
/// </summary>
|
||||
public const string MakeInternal = "Make internal.";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Api11ToDoAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="what">The explanation.</param>
|
||||
/// <param name="what2">The explanation 2.</param>
|
||||
public Api11ToDoAttribute(string what, string what2 = "")
|
||||
{
|
||||
_ = what;
|
||||
_ = what2;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue