diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index f7f144f87..9451f1c05 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -240,6 +240,11 @@ namespace Dalamud.Configuration.Internal
///
public bool DisableRmtFiltering { get; set; }
+ ///
+ /// Gets or sets the order of DTR elements, by title.
+ ///
+ public List? DtrOrder { get; set; }
+
///
/// Load a configuration from the provided path.
///
diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs
new file mode 100644
index 000000000..18b75cfb4
--- /dev/null
+++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs
@@ -0,0 +1,245 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using FFXIVClientStructs.FFXIV.Client.System.Memory;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using Serilog;
+
+namespace Dalamud.Game.Gui.Dtr
+{
+ ///
+ /// Class used to interface with the server info bar.
+ ///
+ [PluginInterface]
+ [InterfaceVersion("1.0")]
+ public unsafe class DtrBar : IDisposable
+ {
+ ///
+ /// The amount of padding between Server Info UI elements.
+ ///
+ private const int ElementPadding = 30;
+
+ private List entries = new();
+ private uint runningNodeIds = 1000;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DtrBar()
+ {
+ Service.Get().Update += this.Update;
+ }
+
+ ///
+ /// Get a DTR bar entry.
+ /// This allows you to add your own text, and users to sort it.
+ ///
+ /// 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 DtrBarEntry Get(string title, SeString? text = null)
+ {
+ if (this.entries.Any(x => x.Title == title))
+ throw new ArgumentException("An entry with the same title already exists.");
+
+ var node = this.MakeNode(++this.runningNodeIds);
+ var entry = new DtrBarEntry(title, node);
+ entry.Text = text;
+
+ this.entries.Add(entry);
+ this.ApplySort();
+
+ return entry;
+ }
+
+ ///
+ void IDisposable.Dispose()
+ {
+ foreach (var entry in this.entries)
+ this.RemoveNode(entry.TextNode);
+
+ this.entries.Clear();
+ Service.Get().Update -= this.Update;
+ }
+
+ ///
+ /// Check whether an entry with the specified title exists.
+ ///
+ /// 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);
+
+ private static AtkUnitBase* GetDtr() => (AtkUnitBase*)Service.Get().GetAddonByName("_DTR", 1).ToPointer();
+
+ private void Update(Framework unused)
+ {
+ var dtr = GetDtr();
+ if (dtr == null) return;
+
+ foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
+ {
+ this.RemoveNode(data.TextNode);
+ }
+
+ this.entries.RemoveAll(d => d.ShouldBeRemoved);
+
+ // The collision node on the DTR element is always the width of its content
+ var collisionNode = dtr->UldManager.NodeList[1];
+ var runningXPos = collisionNode->X;
+
+ for (var i = 0; i < this.entries.Count; i++)
+ {
+ var data = this.entries[i];
+
+ if (data.Dirty && data.Added && data.Text != null && data.TextNode != null)
+ {
+ var node = data.TextNode;
+ node->SetText(data.Text?.Encode());
+ ushort w = 0, h = 0;
+
+ if (!data.Shown)
+ {
+ node->AtkResNode.ToggleVisibility(false);
+ }
+ else
+ {
+ node->AtkResNode.ToggleVisibility(true);
+ node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
+ node->AtkResNode.SetWidth(w);
+ }
+
+ data.Dirty = false;
+ }
+
+ if (!data.Added)
+ {
+ data.Added = this.AddNode(data.TextNode);
+ }
+
+ if (data.Shown)
+ {
+ runningXPos -= data.TextNode->AtkResNode.Width + ElementPadding;
+ data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
+ }
+
+ this.entries[i] = data;
+ }
+ }
+
+ private bool AddNode(AtkTextNode* node)
+ {
+ var dtr = GetDtr();
+ if (dtr == null || dtr->RootNode == null || node == null) return false;
+
+ var lastChild = dtr->RootNode->ChildNode;
+ while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
+ Log.Debug($"Found last sibling: {(ulong)lastChild:X}");
+ lastChild->PrevSiblingNode = (AtkResNode*)node;
+ node->AtkResNode.ParentNode = lastChild->ParentNode;
+ node->AtkResNode.NextSiblingNode = lastChild;
+
+ dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount + 1);
+ Log.Debug("Set last sibling of DTR and updated child count");
+
+ dtr->UldManager.UpdateDrawNodeList();
+ Log.Debug("Updated node draw list");
+ return true;
+ }
+
+ private bool RemoveNode(AtkTextNode* node)
+ {
+ var dtr = GetDtr();
+ if (dtr == null || dtr->RootNode == null || node == null) return false;
+
+ var tmpPrevNode = node->AtkResNode.PrevSiblingNode;
+ var tmpNextNode = node->AtkResNode.NextSiblingNode;
+
+ // if (tmpNextNode != null)
+ tmpNextNode->PrevSiblingNode = tmpPrevNode;
+ if (tmpPrevNode != null)
+ tmpPrevNode->NextSiblingNode = tmpNextNode;
+ node->AtkResNode.Destroy(true);
+
+ dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1);
+ Log.Debug("Set last sibling of DTR and updated child count");
+ dtr->UldManager.UpdateDrawNodeList();
+ Log.Debug("Updated node draw list");
+ return true;
+ }
+
+ private AtkTextNode* MakeNode(uint nodeId)
+ {
+ var newTextNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8);
+ if (newTextNode == null)
+ {
+ Log.Debug("Failed to allocate memory for text node");
+ return null;
+ }
+
+ IMemorySpace.Memset(newTextNode, 0, (ulong)sizeof(AtkTextNode));
+ newTextNode->Ctor();
+
+ newTextNode->AtkResNode.NodeID = nodeId;
+ newTextNode->AtkResNode.Type = NodeType.Text;
+ newTextNode->AtkResNode.Flags = (short)(NodeFlags.AnchorLeft | NodeFlags.AnchorTop);
+ newTextNode->AtkResNode.DrawFlags = 12;
+ newTextNode->AtkResNode.SetWidth(22);
+ newTextNode->AtkResNode.SetHeight(22);
+ newTextNode->AtkResNode.SetPositionFloat(-200, 2);
+
+ newTextNode->LineSpacing = 12;
+ newTextNode->AlignmentFontType = 5;
+ newTextNode->FontSize = 14;
+ newTextNode->TextFlags = (byte)TextFlags.Edge;
+ newTextNode->TextFlags2 = 0;
+
+ newTextNode->SetText(" ");
+
+ newTextNode->TextColor.R = 255;
+ newTextNode->TextColor.G = 255;
+ newTextNode->TextColor.B = 255;
+ newTextNode->TextColor.A = 255;
+
+ newTextNode->EdgeColor.R = 142;
+ newTextNode->EdgeColor.G = 106;
+ newTextNode->EdgeColor.B = 12;
+ newTextNode->EdgeColor.A = 255;
+
+ return newTextNode;
+ }
+
+ private void ApplySort()
+ {
+ var configuration = Service.Get();
+ if (configuration.DtrOrder == null)
+ {
+ configuration.DtrOrder = new List();
+ configuration.Save();
+ }
+
+ // Sort the current entry list, based on the order in the configuration.
+ var ordered = configuration.DtrOrder.Select(entry => this.entries.FirstOrDefault(x => x.Title == entry)).Where(value => value != null).ToList();
+
+ // Add entries that weren't sorted to the end of the list.
+ if (ordered.Count != this.entries.Count)
+ {
+ ordered.AddRange(this.entries.Where(x => ordered.All(y => y.Title != x.Title)));
+ }
+
+ // Update the order list for new entries.
+ configuration.DtrOrder.Clear();
+ foreach (var dtrEntry in ordered)
+ {
+ configuration.DtrOrder.Add(dtrEntry.Title);
+ }
+
+ this.entries = ordered;
+ }
+ }
+}
diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
new file mode 100644
index 000000000..f26344507
--- /dev/null
+++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
@@ -0,0 +1,93 @@
+using System;
+
+using Dalamud.Game.Text.SeStringHandling;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Gui.Dtr
+{
+ ///
+ /// Class representing an entry in the server info bar.
+ ///
+ public unsafe class DtrBarEntry : IDisposable
+ {
+ private bool shownBacking = true;
+ private SeString? textBacking = null;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The title of the bar entry.
+ /// The corresponding text node.
+ internal DtrBarEntry(string title, AtkTextNode* textNode)
+ {
+ this.Title = title;
+ this.TextNode = textNode;
+ }
+
+ ///
+ /// Gets the title of this entry.
+ ///
+ public string Title { get; init; }
+
+ ///
+ /// Gets or sets the text of this entry.
+ ///
+ public SeString? Text
+ {
+ get => this.textBacking;
+ set
+ {
+ this.textBacking = value;
+ this.Dirty = true;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this entry is visible.
+ ///
+ public bool Shown
+ {
+ get => this.shownBacking;
+ set
+ {
+ this.shownBacking = value;
+ this.Dirty = true;
+ }
+ }
+
+ ///
+ /// Gets or sets the internal text node of this entry.
+ ///
+ internal AtkTextNode* TextNode { get; set; }
+
+ ///
+ /// Gets a value indicating whether this entry should be removed.
+ ///
+ internal bool ShouldBeRemoved { get; private set; } = false;
+
+ ///
+ /// Gets or sets a value indicating whether this entry is dirty.
+ ///
+ internal bool Dirty { get; set; } = false;
+
+ ///
+ /// Gets or sets a value indicating whether this entry has just been added.
+ ///
+ internal bool Added { get; set; } = false;
+
+ ///
+ /// Remove this entry from the bar.
+ /// You will need to re-acquire it from DtrBar to reuse it.
+ ///
+ public void Remove()
+ {
+ this.ShouldBeRemoved = true;
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.Remove();
+ }
+ }
+}
diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs
index 78a199b20..6feee6996 100644
--- a/Dalamud/Game/Gui/GameGui.cs
+++ b/Dalamud/Game/Gui/GameGui.cs
@@ -3,6 +3,7 @@ using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Game.Gui.ContextMenus;
+using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Gui.FlyText;
using Dalamud.Game.Gui.PartyFinder;
using Dalamud.Game.Gui.Toast;
@@ -65,6 +66,7 @@ namespace Dalamud.Game.Gui
Service.Set();
Service.Set();
Service.Set();
+ Service.Set();
this.setGlobalBgmHook = new Hook(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour);
@@ -462,6 +464,7 @@ namespace Dalamud.Game.Gui
Service.Get().ExplicitDispose();
Service.Get().ExplicitDispose();
Service.Get().ExplicitDispose();
+ Service.Get().ExplicitDispose();
this.setGlobalBgmHook.Dispose();
this.handleItemHoverHook.Dispose();
this.handleItemOutHook.Dispose();
diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs
index b5d9684bd..ecb915acf 100644
--- a/Dalamud/Interface/Internal/Windows/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs
@@ -25,6 +25,7 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Party;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
+using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Gui.FlyText;
using Dalamud.Game.Gui.Toast;
using Dalamud.Game.Text;
@@ -108,6 +109,10 @@ namespace Dalamud.Interface.Internal.Windows
private Vector4 inputTintCol = Vector4.One;
private Vector2 inputTexScale = Vector2.Zero;
+ // DTR
+ private DtrBarEntry? dtrTest1;
+ private DtrBarEntry? dtrTest2;
+
private uint copyButtonIndex = 0;
///
@@ -158,6 +163,7 @@ namespace Dalamud.Interface.Internal.Windows
TaskSched,
Hook,
Aetherytes,
+ Dtr_Bar,
}
///
@@ -337,9 +343,14 @@ namespace Dalamud.Interface.Internal.Windows
case DataKind.Hook:
this.DrawHook();
break;
+
case DataKind.Aetherytes:
this.DrawAetherytes();
break;
+
+ case DataKind.Dtr_Bar:
+ this.DrawDtr();
+ break;
}
}
else
@@ -1588,6 +1599,76 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.EndTable();
}
+ private void DrawDtr()
+ {
+ var dtrBar = Service.Get();
+
+ if (this.dtrTest1 != null)
+ {
+ ImGui.Text("DtrTest1");
+
+ var text = this.dtrTest1.Text?.TextValue ?? string.Empty;
+ if (ImGui.InputText("Text###dtr1t", ref text, 255))
+ this.dtrTest1.Text = text;
+
+ var shown = this.dtrTest1.Shown;
+ if (ImGui.Checkbox("Shown###dtr1s", ref shown))
+ this.dtrTest1.Shown = shown;
+
+ if (ImGui.Button("Remove###dtr1r"))
+ {
+ this.dtrTest1.Remove();
+ this.dtrTest1 = null;
+ }
+ }
+ else
+ {
+ if (ImGui.Button("Add #1"))
+ {
+ this.dtrTest1 = dtrBar.Get("DTR Test #1");
+ }
+ }
+
+ ImGui.Separator();
+
+ if (this.dtrTest2 != null)
+ {
+ ImGui.Text("DtrTest2");
+
+ var text = this.dtrTest2.Text?.TextValue ?? string.Empty;
+ if (ImGui.InputText("Text###dtr2t", ref text, 255))
+ this.dtrTest2.Text = text;
+
+ var shown = this.dtrTest2.Shown;
+ if (ImGui.Checkbox("Shown###dtr2s", ref shown))
+ this.dtrTest2.Shown = shown;
+
+ if (ImGui.Button("Remove###dtr2r"))
+ {
+ this.dtrTest2.Remove();
+ this.dtrTest2 = null;
+ }
+ }
+ else
+ {
+ if (ImGui.Button("Add #2"))
+ {
+ this.dtrTest2 = dtrBar.Get("DTR Test #2");
+ }
+ }
+
+ var configuration = Service.Get();
+ if (configuration.DtrOrder != null)
+ {
+ ImGui.Separator();
+
+ foreach (var order in configuration.DtrOrder)
+ {
+ ImGui.Text(order);
+ }
+ }
+ }
+
private async Task TestTaskInTaskDelay()
{
await Task.Delay(5000);