From 2a65d1e045ed102c141040110f303a6ee4a51e1a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 7 Nov 2025 16:04:13 +0100 Subject: [PATCH 1/9] Fix KeyNotFoundException in DtrBar.RemoveNode --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index bd8d8e1d7..920978b00 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -546,9 +546,12 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar { var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - - this.eventHandles[node->AtkResNode.NodeId].ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); - this.eventHandles[node->AtkResNode.NodeId].Clear(); + + if (this.eventHandles.TryGetValue(node->AtkResNode.NodeId, out var eventHandles)) + { + eventHandles.ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); + eventHandles.Clear(); + } var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; From dabe7d777be88b98f7adffac293302507915c9bf Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 11 Nov 2025 19:27:46 +0100 Subject: [PATCH 2/9] build: 13.0.0.8 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 890202967..1166c24c0 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.7 + 13.0.0.8 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 65237f84a22a561d719f57ffb1cc8013f70f20ef Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 10 Nov 2025 04:41:01 +0100 Subject: [PATCH 3/9] Add functions to get a plugin by assembly This is intended for advanced IPC scenarios, for example, accepting a delegate or an object and identifying which plugin it originates from, in order to display integration information to the user, and/or to release references when the originating plugin is unloaded/reloaded if it forgot to clean after itself. --- Dalamud/Plugin/DalamudPluginInterface.cs | 25 ++++++++++++++++++++ Dalamud/Plugin/IDalamudPluginInterface.cs | 16 +++++++++++++ Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 9 +++++++ 3 files changed, 50 insertions(+) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 39a4e7e4b..957e8080a 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.Loader; using System.Threading.Tasks; using Dalamud.Configuration; @@ -269,6 +270,30 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa return true; } + /// + /// Gets the plugin the given assembly is part of. + /// + /// The assembly to check. + /// The plugin the given assembly is part of, or null if this is a shared assembly or if this information cannot be determined. + public IExposedPlugin? GetPlugin(Assembly assembly) + => AssemblyLoadContext.GetLoadContext(assembly) switch + { + null => null, + var context => this.GetPlugin(context), + }; + + /// + /// Gets the plugin that loads in the given context. + /// + /// The context to check. + /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. + public IExposedPlugin? GetPlugin(AssemblyLoadContext context) + => Service.Get().InstalledPlugins.FirstOrDefault(p => p.LoadsIn(context)) switch + { + null => null, + var p => new ExposedPlugin(p), + }; + #region IPC /// diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index b8ab55450..e1dd34f87 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reflection; +using System.Runtime.Loader; using System.Threading.Tasks; using Dalamud.Configuration; @@ -180,6 +182,20 @@ public interface IDalamudPluginInterface /// Returns false if the DalamudInterface was null. bool OpenDeveloperMenu(); + /// + /// Gets the plugin the given assembly is part of. + /// + /// The assembly to check. + /// The plugin the given assembly is part of, or null if this is a shared assembly or if this information cannot be determined. + IExposedPlugin? GetPlugin(Assembly assembly); + + /// + /// Gets the plugin that loads in the given context. + /// + /// The context to check. + /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. + IExposedPlugin? GetPlugin(AssemblyLoadContext context); + /// T GetOrCreateData(string tag, Func dataGenerator) where T : class; diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index da8ec8ff9..0197683ef 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; +using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; @@ -553,6 +554,14 @@ internal class LocalPlugin : IAsyncDisposable }); } + /// + /// Checks whether this plugin loads in the given load context. + /// + /// The load context to check. + /// Whether this plugin loads in the given load context. + public bool LoadsIn(AssemblyLoadContext context) + => this.loader?.LoadContext == context; + /// /// Save this plugin manifest. /// From f6cd6d31ff26e6d7823f7e37bdde7a36eb4fb3dd Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 11 Nov 2025 20:29:06 +0100 Subject: [PATCH 4/9] Adjust branch switcher to XL 7, pass beta kind and key as arguments --- .../Internal/Windows/BranchSwitcherWindow.cs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index 5caf8b5be..f7250e528 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -83,25 +83,12 @@ public class BranchSwitcherWindow : Window ImGuiHelpers.ScaledDummy(5); - void Pick() + if (ImGui.Button("Pick & Restart"u8)) { var config = Service.Get(); config.DalamudBetaKind = pickedBranch.Key; config.DalamudBetaKey = pickedBranch.Value.Key; config.QueueSave(); - } - - if (ImGui.Button("Pick"u8)) - { - Pick(); - this.IsOpen = false; - } - - ImGui.SameLine(); - - if (ImGui.Button("Pick & Restart"u8)) - { - Pick(); // If we exit immediately, we need to write out the new config now Service.Get().ForceSave(); @@ -111,7 +98,16 @@ public class BranchSwitcherWindow : Window if (File.Exists(xlPath)) { - Process.Start(xlPath); + var ps = new ProcessStartInfo + { + FileName = xlPath, + UseShellExecute = false, + }; + + ps.ArgumentList.Add($"--dalamud-beta-kind={config.DalamudBetaKind}"); + ps.ArgumentList.Add($"--dalamud-beta-key={config.DalamudBetaKey}"); + + Process.Start(ps); Environment.Exit(0); } } From 4cfe561c1c56f419eb23e7752315422909379397 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 11 Nov 2025 21:28:30 +0100 Subject: [PATCH 5/9] Add a property to get the bounds of a DTR entry --- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index 6fdc504ca..f5b7011fe 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -1,4 +1,6 @@ -using Dalamud.Configuration.Internal; +using System.Numerics; + +using Dalamud.Configuration.Internal; using Dalamud.Game.Addon.Events.EventDataTypes; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Internal.Types; @@ -48,6 +50,11 @@ public interface IReadOnlyDtrBarEntry /// Gets an action to be invoked when the user clicks on the dtr entry. /// public Action? OnClick { get; } + + /// + /// Gets the axis-aligned bounding box of this entry, in screen coordinates. + /// + public (Vector2 Min, Vector2 Max) ScreenBounds { get; } } /// @@ -146,6 +153,17 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry [Api13ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; + /// + public (Vector2 Min, Vector2 Max) ScreenBounds + => this.TextNode switch + { + null => default, + var node => node->IsVisible() + ? (new(node->ScreenX, node->ScreenY), + new(node->ScreenX + node->GetWidth(), node->ScreenY + node->GetHeight())) + : default, + }; + /// /// Gets or sets the internal text node of this entry. /// From bf0bd64faff9c66f2d0e5bb7094658d3aafaa9a5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 11 Nov 2025 23:37:10 +0100 Subject: [PATCH 6/9] Fix IsAutoUpdateComplete throwing when unloaded --- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 39a4e7e4b..53115d179 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -103,7 +103,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// /// Gets a value indicating whether auto-updates have already completed this session. /// - public bool IsAutoUpdateComplete => Service.Get().IsAutoUpdateComplete; + public bool IsAutoUpdateComplete => Service.GetNullable()?.IsAutoUpdateComplete ?? false; /// /// Gets the repository from which this plugin was installed. From e1fde804ecf50f57b91e27a8a8e4863eb24f580a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 12 Nov 2025 00:21:47 +0100 Subject: [PATCH 7/9] Add warning if RemoveNode can't find the node --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 920978b00..dabe292bd 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -546,12 +546,16 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar { var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - + if (this.eventHandles.TryGetValue(node->AtkResNode.NodeId, out var eventHandles)) { eventHandles.ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); eventHandles.Clear(); } + else + { + Log.Warning("Could not find AtkResNode with NodeId {nodeId} in eventHandles", node->AtkResNode.NodeId); + } var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; From f635c149a236389934bd7a6850843d3612589772 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 12 Nov 2025 00:23:08 +0100 Subject: [PATCH 8/9] Set TextNode to null after destroying it --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index dabe292bd..b5c671a6f 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -257,7 +257,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar /// The resources to remove. internal void RemoveEntry(DtrBarEntry toRemove) { - this.RemoveNode(toRemove.TextNode); + this.RemoveNode(toRemove); if (toRemove.Storage != null) { @@ -542,9 +542,10 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar return true; } - private void RemoveNode(AtkTextNode* node) + private void RemoveNode(DtrBarEntry data) { var dtr = this.GetDtr(); + var node = data.TextNode; if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; if (this.eventHandles.TryGetValue(node->AtkResNode.NodeId, out var eventHandles)) @@ -565,6 +566,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar if (tmpPrevNode != null) tmpPrevNode->NextSiblingNode = tmpNextNode; node->AtkResNode.Destroy(true); + data.TextNode = null; dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1); Log.Debug("Set last sibling of DTR and updated child count"); From 45bd30fccac0702981bdf7509353616c3f30cc4e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 12 Nov 2025 00:27:51 +0100 Subject: [PATCH 9/9] Use inherited fields --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 48 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index b5c671a6f..2235c5ade 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -378,12 +378,12 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar var isHide = !data.Shown || data.UserHidden; var node = data.TextNode; - var nodeHidden = !node->AtkResNode.IsVisible(); + var nodeHidden = !node->IsVisible(); if (!isHide) { if (nodeHidden) - node->AtkResNode.ToggleVisibility(true); + node->ToggleVisibility(true); if (data is { Added: true, Text: not null, TextNode: not null } && (data.Dirty || nodeHidden)) { @@ -397,27 +397,27 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar ushort w = 0, h = 0; node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); - node->AtkResNode.SetWidth(w); + node->SetWidth(w); } - var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing; + var elementWidth = data.TextNode->Width + this.configuration.DtrSpacing; if (this.configuration.DtrSwapDirection) { - data.TextNode->AtkResNode.SetPositionFloat(runningXPos + this.configuration.DtrSpacing, 2); + data.TextNode->SetPositionFloat(runningXPos + this.configuration.DtrSpacing, 2); runningXPos += elementWidth; } else { runningXPos -= elementWidth; - data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); + data.TextNode->SetPositionFloat(runningXPos, 2); } } else if (!nodeHidden) { // If we want the node hidden, shift it up, to prevent collision conflicts - node->AtkResNode.SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX); - node->AtkResNode.ToggleVisibility(false); + node->SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX); + node->ToggleVisibility(false); } data.Dirty = false; @@ -516,8 +516,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar var node = data.TextNode = this.MakeNode(++this.runningNodeIds); - this.eventHandles.TryAdd(node->AtkResNode.NodeId, new List()); - this.eventHandles[node->AtkResNode.NodeId].AddRange(new List + this.eventHandles.TryAdd(node->NodeId, new List()); + this.eventHandles[node->NodeId].AddRange(new List { this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler), this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler), @@ -528,8 +528,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar 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; + node->ParentNode = lastChild->ParentNode; + node->NextSiblingNode = lastChild; dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount + 1); Log.Debug("Set last sibling of DTR and updated child count"); @@ -548,24 +548,24 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar var node = data.TextNode; if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - if (this.eventHandles.TryGetValue(node->AtkResNode.NodeId, out var eventHandles)) + if (this.eventHandles.TryGetValue(node->NodeId, out var eventHandles)) { eventHandles.ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); eventHandles.Clear(); } else { - Log.Warning("Could not find AtkResNode with NodeId {nodeId} in eventHandles", node->AtkResNode.NodeId); + Log.Warning("Could not find AtkResNode with NodeId {nodeId} in eventHandles", node->NodeId); } - var tmpPrevNode = node->AtkResNode.PrevSiblingNode; - var tmpNextNode = node->AtkResNode.NextSiblingNode; + var tmpPrevNode = node->PrevSiblingNode; + var tmpNextNode = node->NextSiblingNode; // if (tmpNextNode != null) tmpNextNode->PrevSiblingNode = tmpPrevNode; if (tmpPrevNode != null) tmpPrevNode->NextSiblingNode = tmpNextNode; - node->AtkResNode.Destroy(true); + node->Destroy(true); data.TextNode = null; dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1); @@ -584,13 +584,13 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar return null; } - newTextNode->AtkResNode.NodeId = nodeId; - newTextNode->AtkResNode.Type = NodeType.Text; - newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents; - newTextNode->AtkResNode.DrawFlags = 12; - newTextNode->AtkResNode.SetWidth(22); - newTextNode->AtkResNode.SetHeight(22); - newTextNode->AtkResNode.SetPositionFloat(-200, 2); + newTextNode->NodeId = nodeId; + newTextNode->Type = NodeType.Text; + newTextNode->NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents; + newTextNode->DrawFlags = 12; + newTextNode->SetWidth(22); + newTextNode->SetHeight(22); + newTextNode->SetPositionFloat(-200, 2); newTextNode->LineSpacing = 12; newTextNode->AlignmentFontType = 5;