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:
srkizer 2024-07-28 21:14:37 +09:00 committed by GitHub
parent a7ab3b9def
commit c25f13261d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 385 additions and 207 deletions

View file

@ -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()
{

View file

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

View file

@ -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()
{

View file

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

View file

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

View file

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

View file

@ -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();
}
}

View file

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

View file

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

View 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;
}
}