diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 50f4e81ce..c0b9c6e12 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -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() { diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 55b2573f0..04e7fea07 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -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 newEntries = new(); - private readonly List entries = new(); + private readonly ReaderWriterLockSlim entriesLock = new(); + private readonly List entries = []; private readonly Dictionary> eventHandles = new(); + private ImmutableList? 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(); - this.configuration.DtrIgnore ??= new List(); + this.configuration.DtrOrder ??= []; + this.configuration.DtrIgnore ??= []; this.configuration.QueueSave(); } - /// - /// Event type fired each time a DtrEntry was removed. - /// - /// The title of the bar entry. - internal delegate void DtrEntryRemovedDelegate(string title); - - /// - /// Event fired each time a DtrEntry was removed. - /// - internal event DtrEntryRemovedDelegate? DtrEntryRemoved; - /// - public IReadOnlyList Entries => this.entries; - - /// - public IDtrBarEntry Get(string title, SeString? text = null) + public IReadOnlyList 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; - } - - /// - 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; } } + /// + /// Get a DTR bar entry. + /// This allows you to add your own text, and users to sort it. + /// + /// Plugin that owns the DTR bar, or null if owned by Dalamud. + /// A user-friendly name for sorting. + /// The text the entry shows. + /// The entry object used to update, hide and remove the entry. + /// Thrown when an entry with the specified title exists. + 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; + } + + /// + public IDtrBarEntry Get(string title, SeString? text = null) => this.Get(null, title, text); + + /// + /// Removes a DTR bar entry from the system. + /// + /// Plugin that owns the DTR bar, or null if owned by Dalamud. + /// Title of the entry to remove, or null to remove all entries under the plugin. + /// Remove operation is not immediate. If you try to add right after removing, the operation may fail. + /// + 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(); + } + + /// + public void Remove(string title) => this.Remove(null, title); + /// 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 } } - /// - /// Remove nodes marked as "should be removed" from the bar. - /// - 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); - } - /// /// Remove native resources for the specified entry. /// @@ -174,7 +224,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar /// /// The title to check for. /// Whether or not an entry with that title is registered. - 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; + } /// /// Dirty the DTR bar entry with the specified title. @@ -183,24 +243,37 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar /// Whether the entry was found. 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; } /// /// Reapply the DTR entry ordering from . /// internal void ApplySort() + { + this.entriesLock.EnterWriteLock(); + this.ApplySortUnsafe(this.configuration.DtrOrder ??= []); + this.entriesLock.ExitWriteLock(); + } + + private void ApplySortUnsafe(List 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()); this.eventHandles[node->AtkResNode.NodeId].AddRange(new List @@ -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] #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.Get(); - private readonly Dictionary pluginEntries = new(); - - /// - /// Initializes a new instance of the class. - /// - internal DtrBarPluginScoped() - { - this.dtrBarService.DtrEntryRemoved += this.OnDtrEntryRemoved; - } + [ServiceManager.ServiceConstructor] + private DtrBarPluginScoped(LocalPlugin plugin) => this.plugin = plugin; /// public IReadOnlyList Entries => this.dtrBarService.Entries; /// - 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); /// - 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); - } - /// - 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); } diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index fc5210fda..a2fa73ec6 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -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 /// /// Class representing an entry in the server info bar. /// +[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 } /// - [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; /// @@ -160,9 +160,9 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry internal Utf8String* Storage { get; set; } /// - /// Gets a value indicating whether this entry should be removed. + /// Gets or sets a value indicating whether this entry should be removed. /// - internal bool ShouldBeRemoved { get; private set; } + internal bool ShouldBeRemoved { get; set; } /// /// Gets or sets a value indicating whether this entry is dirty. @@ -174,6 +174,11 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry /// internal bool Added { get; set; } + /// + /// Gets or sets the plugin that owns this entry. + /// + internal LocalPlugin? OwnerPlugin { get; set; } + /// public bool TriggerClickAction() { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs index 1f58541be..8b7a692d4 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs @@ -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; /// /// Widget for displaying dtr test. /// -internal class DtrBarWidget : IDataWindowWidget +internal class DtrBarWidget : IDataWindowWidget, IDisposable { private IDtrBarEntry? dtrTest1; private IDtrBarEntry? dtrTest2; private IDtrBarEntry? dtrTest3; - + + private Thread? loadTestThread; + private CancellationTokenSource? loadTestThreadCt; + /// public string[]? CommandShortcuts { get; init; } = { "dtr", "dtrbar" }; - + /// - public string DisplayName { get; init; } = "DTR Bar"; + public string DisplayName { get; init; } = "DTR Bar"; /// public bool Ready { get; set; } @@ -26,31 +33,145 @@ internal class DtrBarWidget : IDataWindowWidget /// public void Load() { + this.ClearState(); this.Ready = true; } + /// + public void Dispose() => this.ClearState(); + /// 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.Get(); + var fw = Service.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.Get().Entries) + ImGui.Text(e.Title); var configuration = Service.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.Get(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs index 2559911cf..d1040b5b2 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs @@ -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 upButtonCenters = stackalloc Vector2[order.Count]; + Span downButtonCenters = stackalloc Vector2[order.Count]; + scoped Span 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(); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 5d365d66c..910472f5f 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -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.Get(); - dtr.HandleRemovedNodes(); - try { await this.InstallPluginInternalAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update, updateStream, workingPluginId); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 00fa9d243..b94c40918 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -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.GetNullable()?.HandleRemovedNodes(); - this.pluginLoadStateLock.Release(); } } diff --git a/Dalamud/Plugin/Services/IDtrBar.cs b/Dalamud/Plugin/Services/IDtrBar.cs index 733a6d7e1..8ab34c6f2 100644 --- a/Dalamud/Plugin/Services/IDtrBar.cs +++ b/Dalamud/Plugin/Services/IDtrBar.cs @@ -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 { /// - /// Gets a read-only list of all DTR bar entries. + /// Gets a read-only copy of the list of all DTR bar entries. /// - public IReadOnlyList Entries { get; } - + /// 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. + IReadOnlyList Entries { get; } + /// /// 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 /// The text the entry shows. /// The entry object used to update, hide and remove the entry. /// Thrown when an entry with the specified title exists. - public IDtrBarEntry Get(string title, SeString? text = null); + IDtrBarEntry Get(string title, SeString? text = null); /// /// Removes a DTR bar entry from the system. /// /// Title of the entry to remove. - public void Remove(string title); + /// Remove operation is not guaranteed to be immediately effective. Calls to may result + /// in an entry marked to be remove being revived and used again. + void Remove(string title); } diff --git a/Dalamud/Utility/Api10ToDoAttribute.cs b/Dalamud/Utility/Api10ToDoAttribute.cs deleted file mode 100644 index a13aaead5..000000000 --- a/Dalamud/Utility/Api10ToDoAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Dalamud.Utility; - -/// -/// Utility class for marking something to be changed for API 10, for ease of lookup. -/// -[AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api10ToDoAttribute : Attribute -{ - /// - /// Marks that this exists purely for making API 9 plugins work. - /// - public const string DeleteCompatBehavior = "Delete. This is for making API 9 plugins work."; - - /// - /// Marks that this should be moved to an another namespace. - /// - public const string MoveNamespace = "Move to another namespace."; - - /// - /// Initializes a new instance of the class. - /// - /// The explanation. - /// The explanation 2. - public Api10ToDoAttribute(string what, string what2 = "") - { - _ = what; - _ = what2; - } -} diff --git a/Dalamud/Utility/Api11ToDoAttribute.cs b/Dalamud/Utility/Api11ToDoAttribute.cs new file mode 100644 index 000000000..0336120ba --- /dev/null +++ b/Dalamud/Utility/Api11ToDoAttribute.cs @@ -0,0 +1,24 @@ +namespace Dalamud.Utility; + +/// +/// Utility class for marking something to be changed for API 11, for ease of lookup. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class Api11ToDoAttribute : Attribute +{ + /// + /// Marks that this should be made internal. + /// + public const string MakeInternal = "Make internal."; + + /// + /// Initializes a new instance of the class. + /// + /// The explanation. + /// The explanation 2. + public Api11ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } +}