Compare commits

...

13 commits

Author SHA1 Message Date
goat
b11b769292
Merge pull request #2453 from Exter-N/get-plugin-by-assembly
Some checks failed
Tag Build / Tag Build (push) Successful in 3s
Build Dalamud / Build on Windows (push) Has been cancelled
Build Dalamud / Check API Compatibility (push) Has been cancelled
Build Dalamud / Deploy dalamud-distrib staging (push) Has been cancelled
Add functions to get a plugin by assembly
2025-11-12 01:33:24 +01:00
goat
dc9ff0a54c
Merge pull request #2448 from Haselnussbomber/dtr-removenode-exception-fix
Fix KeyNotFoundException in DtrBar.RemoveNode
2025-11-12 00:49:40 +01:00
Haselnussbomber
45bd30fcca
Use inherited fields 2025-11-12 00:27:51 +01:00
Haselnussbomber
f635c149a2
Set TextNode to null after destroying it 2025-11-12 00:26:06 +01:00
Haselnussbomber
e1fde804ec
Add warning if RemoveNode can't find the node 2025-11-12 00:21:47 +01:00
goat
bcce4f4216
Merge pull request #2455 from Exter-N/dtr-screen-bounds
Add a property to get the bounds of a DTR entry
2025-11-12 00:04:50 +01:00
goat
9450f65159
Merge pull request #2456 from Haselnussbomber/autoupdatenullable
Fix IsAutoUpdateComplete throwing when unloaded
2025-11-11 23:55:14 +01:00
Haselnussbomber
bf0bd64faf
Fix IsAutoUpdateComplete throwing when unloaded 2025-11-11 23:37:10 +01:00
Exter-N
4cfe561c1c Add a property to get the bounds of a DTR entry 2025-11-11 21:28:30 +01:00
goaaats
f6cd6d31ff Adjust branch switcher to XL 7, pass beta kind and key as arguments 2025-11-11 20:29:06 +01:00
Exter-N
65237f84a2 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.
2025-11-11 20:26:54 +01:00
goaaats
dabe7d777b build: 13.0.0.8 2025-11-11 19:27:46 +01:00
Haselnussbomber
2a65d1e045
Fix KeyNotFoundException in DtrBar.RemoveNode 2025-11-07 16:04:13 +01:00
7 changed files with 117 additions and 44 deletions

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>13.0.0.7</DalamudVersion>
<DalamudVersion>13.0.0.8</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>

View file

@ -257,7 +257,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
/// <param name="toRemove">The resources to remove.</param>
internal void RemoveEntry(DtrBarEntry toRemove)
{
this.RemoveNode(toRemove.TextNode);
this.RemoveNode(toRemove);
if (toRemove.Storage != null)
{
@ -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<IAddonEventHandle>());
this.eventHandles[node->AtkResNode.NodeId].AddRange(new List<IAddonEventHandle>
this.eventHandles.TryAdd(node->NodeId, new List<IAddonEventHandle>());
this.eventHandles[node->NodeId].AddRange(new List<IAddonEventHandle>
{
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");
@ -542,22 +542,31 @@ 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;
this.eventHandles[node->AtkResNode.NodeId].ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle));
this.eventHandles[node->AtkResNode.NodeId].Clear();
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->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);
Log.Debug("Set last sibling of DTR and updated child count");
@ -575,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;

View file

@ -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.
/// </summary>
public Action<DtrInteractionEvent>? OnClick { get; }
/// <summary>
/// Gets the axis-aligned bounding box of this entry, in screen coordinates.
/// </summary>
public (Vector2 Min, Vector2 Max) ScreenBounds { get; }
}
/// <summary>
@ -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;
/// <inheritdoc/>
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,
};
/// <summary>
/// Gets or sets the internal text node of this entry.
/// </summary>

View file

@ -83,25 +83,12 @@ public class BranchSwitcherWindow : Window
ImGuiHelpers.ScaledDummy(5);
void Pick()
if (ImGui.Button("Pick & Restart"u8))
{
var config = Service<DalamudConfiguration>.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<DalamudConfiguration>.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);
}
}

View file

@ -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;
@ -103,7 +104,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
/// <summary>
/// Gets a value indicating whether auto-updates have already completed this session.
/// </summary>
public bool IsAutoUpdateComplete => Service<AutoUpdateManager>.Get().IsAutoUpdateComplete;
public bool IsAutoUpdateComplete => Service<AutoUpdateManager>.GetNullable()?.IsAutoUpdateComplete ?? false;
/// <summary>
/// Gets the repository from which this plugin was installed.
@ -269,6 +270,30 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
return true;
}
/// <summary>
/// Gets the plugin the given assembly is part of.
/// </summary>
/// <param name="assembly">The assembly to check.</param>
/// <returns>The plugin the given assembly is part of, or null if this is a shared assembly or if this information cannot be determined.</returns>
public IExposedPlugin? GetPlugin(Assembly assembly)
=> AssemblyLoadContext.GetLoadContext(assembly) switch
{
null => null,
var context => this.GetPlugin(context),
};
/// <summary>
/// Gets the plugin that loads in the given context.
/// </summary>
/// <param name="context">The context to check.</param>
/// <returns>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.</returns>
public IExposedPlugin? GetPlugin(AssemblyLoadContext context)
=> Service<PluginManager>.Get().InstalledPlugins.FirstOrDefault(p => p.LoadsIn(context)) switch
{
null => null,
var p => new ExposedPlugin(p),
};
#region IPC
/// <inheritdoc cref="DataShare.GetOrCreateData{T}"/>

View file

@ -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>Returns false if the DalamudInterface was null.</returns>
bool OpenDeveloperMenu();
/// <summary>
/// Gets the plugin the given assembly is part of.
/// </summary>
/// <param name="assembly">The assembly to check.</param>
/// <returns>The plugin the given assembly is part of, or null if this is a shared assembly or if this information cannot be determined.</returns>
IExposedPlugin? GetPlugin(Assembly assembly);
/// <summary>
/// Gets the plugin that loads in the given context.
/// </summary>
/// <param name="context">The context to check.</param>
/// <returns>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.</returns>
IExposedPlugin? GetPlugin(AssemblyLoadContext context);
/// <inheritdoc cref="DataShare.GetOrCreateData{T}"/>
T GetOrCreateData<T>(string tag, Func<T> dataGenerator) where T : class;

View file

@ -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
});
}
/// <summary>
/// Checks whether this plugin loads in the given load context.
/// </summary>
/// <param name="context">The load context to check.</param>
/// <returns>Whether this plugin loads in the given load context.</returns>
public bool LoadsIn(AssemblyLoadContext context)
=> this.loader?.LoadContext == context;
/// <summary>
/// Save this plugin manifest.
/// </summary>