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