From c19ea6ace38f79386f1c4727851dcbcdf23b036f Mon Sep 17 00:00:00 2001
From: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Date: Tue, 1 Jul 2025 19:02:32 +0900
Subject: [PATCH 001/201] Add ITextureProvider.CreateTextureFromSeString
---
.../Internal/SeStringColorStackSet.cs | 32 ++--
.../Internal/SeStringRenderer.cs | 176 +++++++++---------
.../Internal/TextFragment.cs | 39 ++++
.../SeStringDrawState.cs | 99 +++++++---
.../Interface/Internal/DalamudInterface.cs | 7 +
.../Internal/Windows/Data/DataWindow.cs | 31 ++-
.../Data/Widgets/FontAwesomeTestWidget.cs | 26 ++-
.../Widgets/SeStringRendererTestWidget.cs | 46 +++--
.../Windows/Data/Widgets/TexWidget.cs | 12 +-
.../Interface/ManagedFontAtlas/IFontHandle.cs | 24 ++-
.../ManagedFontAtlas/Internals/FontHandle.cs | 7 +-
.../Internal/TextureManager.FromSeString.cs | 35 ++++
.../Textures/Internal/TextureManager.cs | 3 +-
.../Internal/TextureManagerPluginScoped.cs | 13 ++
Dalamud/Interface/UiBuilder.cs | 33 +++-
.../Utility/BufferBackedImDrawData.cs | 112 +++++++++++
Dalamud/Interface/Utility/ImGuiHelpers.cs | 9 +
.../Utility/Internal/DevTextureSaveMenu.cs | 126 ++++++++-----
Dalamud/Plugin/Services/ITextureProvider.cs | 14 +-
19 files changed, 629 insertions(+), 215 deletions(-)
create mode 100644 Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs
create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs
create mode 100644 Dalamud/Interface/Utility/BufferBackedImDrawData.cs
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs
index ad60d405e..85ab2e441 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringColorStackSet.cs
@@ -15,10 +15,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// Color stacks to use while evaluating a SeString.
internal sealed class SeStringColorStackSet
{
- /// Parsed , containing colors to use with and
- /// .
- private readonly uint[,] colorTypes;
-
/// Foreground color stack while evaluating a SeString for rendering.
/// Touched only from the main thread.
private readonly List colorStack = [];
@@ -39,30 +35,38 @@ internal sealed class SeStringColorStackSet
foreach (var row in uiColor)
maxId = (int)Math.Max(row.RowId, maxId);
- this.colorTypes = new uint[maxId + 1, 4];
+ this.ColorTypes = new uint[maxId + 1, 4];
foreach (var row in uiColor)
{
// Contains ABGR.
- this.colorTypes[row.RowId, 0] = row.Dark;
- this.colorTypes[row.RowId, 1] = row.Light;
- this.colorTypes[row.RowId, 2] = row.ClassicFF;
- this.colorTypes[row.RowId, 3] = row.ClearBlue;
+ this.ColorTypes[row.RowId, 0] = row.Dark;
+ this.ColorTypes[row.RowId, 1] = row.Light;
+ this.ColorTypes[row.RowId, 2] = row.ClassicFF;
+ this.ColorTypes[row.RowId, 3] = row.ClearBlue;
}
if (BitConverter.IsLittleEndian)
{
// ImGui wants RGBA in LE.
- fixed (uint* p = this.colorTypes)
+ fixed (uint* p = this.ColorTypes)
{
- foreach (ref var r in new Span(p, this.colorTypes.GetLength(0) * this.colorTypes.GetLength(1)))
+ foreach (ref var r in new Span(p, this.ColorTypes.GetLength(0) * this.ColorTypes.GetLength(1)))
r = BinaryPrimitives.ReverseEndianness(r);
}
}
}
+ /// Initializes a new instance of the class.
+ /// Color types.
+ public SeStringColorStackSet(uint[,] colorTypes) => this.ColorTypes = colorTypes;
+
/// Gets a value indicating whether at least one color has been pushed to the edge color stack.
public bool HasAdditionalEdgeColor { get; private set; }
+ /// Gets the parsed containing colors to use with
+ /// and .
+ public uint[,] ColorTypes { get; }
+
/// Resets the colors in the stack.
/// Draw state.
internal void Initialize(scoped ref SeStringDrawState drawState)
@@ -191,9 +195,9 @@ internal sealed class SeStringColorStackSet
}
// Opacity component is ignored.
- var color = themeIndex >= 0 && themeIndex < this.colorTypes.GetLength(1) &&
- colorTypeIndex < this.colorTypes.GetLength(0)
- ? this.colorTypes[colorTypeIndex, themeIndex]
+ var color = themeIndex >= 0 && themeIndex < this.ColorTypes.GetLength(1) &&
+ colorTypeIndex < this.ColorTypes.GetLength(0)
+ ? this.ColorTypes[colorTypeIndex, themeIndex]
: 0u;
rgbaStack.Add(color | 0xFF000000u);
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
index d0c40cd9f..0099e6e5d 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Numerics;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@@ -25,7 +26,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// Draws SeString.
[ServiceManager.EarlyLoadedService]
-internal unsafe class SeStringRenderer : IInternalDisposableService
+internal class SeStringRenderer : IServiceType
{
private const int ImGuiContextCurrentWindowOffset = 0x3FF0;
private const int ImGuiWindowDcOffset = 0x118;
@@ -47,28 +48,19 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// Parsed text fragments from a SeString.
/// Touched only from the main thread.
- private readonly List fragments = [];
+ private readonly List fragmentsMainThread = [];
/// Color stacks to use while evaluating a SeString for rendering.
/// Touched only from the main thread.
- private readonly SeStringColorStackSet colorStackSet;
-
- /// Splits a draw list so that different layers of a single glyph can be drawn out of order.
- private ImDrawListSplitter* splitter = ImGui.ImDrawListSplitter();
+ private readonly SeStringColorStackSet colorStackSetMainThread;
[ServiceManager.ServiceConstructor]
private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
{
- this.colorStackSet = new(dm.Excel.GetSheet());
+ this.colorStackSetMainThread = new(dm.Excel.GetSheet());
this.gfd = dm.GetFile("common/font/gfdata.gfd")!;
}
- /// Finalizes an instance of the class.
- ~SeStringRenderer() => this.ReleaseUnmanagedResources();
-
- ///
- void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
-
/// Compiles and caches a SeString from a text macro representation.
/// SeString text macro representation.
/// Newline characters will be normalized to newline payloads.
@@ -80,6 +72,44 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
text.ReplaceLineEndings("
"),
new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }));
+ /// Creates a draw data that will draw the given SeString onto it.
+ /// SeString to render.
+ /// Parameters for drawing.
+ /// A new self-contained draw data.
+ public unsafe BufferBackedImDrawData CreateDrawData(
+ ReadOnlySeStringSpan sss,
+ scoped in SeStringDrawParams drawParams = default)
+ {
+ if (drawParams.TargetDrawList is not null)
+ {
+ throw new ArgumentException(
+ $"{nameof(SeStringDrawParams.TargetDrawList)} may not be specified.",
+ nameof(drawParams));
+ }
+
+ var dd = BufferBackedImDrawData.Create();
+
+ try
+ {
+ var size = this.Draw(sss, drawParams with { TargetDrawList = dd.ListPtr }).Size;
+
+ var offset = drawParams.ScreenOffset ?? Vector2.Zero;
+ foreach (var vtx in new Span(dd.ListPtr.VtxBuffer.Data, dd.ListPtr.VtxBuffer.Size))
+ offset = Vector2.Min(offset, vtx.Pos);
+
+ dd.Data.DisplayPos = offset;
+ dd.Data.DisplaySize = size - offset;
+ dd.Data.Valid = 1;
+ dd.UpdateDrawDataStatistics();
+ return dd;
+ }
+ catch
+ {
+ dd.Dispose();
+ throw;
+ }
+ }
+
/// Compiles and caches a SeString from a text macro representation, and then draws it.
/// SeString text macro representation.
/// Newline characters will be normalized to newline payloads.
@@ -113,28 +143,42 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// ImGui ID, if link functionality is desired.
/// Button flags to use on link interaction.
/// Interaction result of the rendered text.
- public SeStringDrawResult Draw(
+ public unsafe SeStringDrawResult Draw(
ReadOnlySeStringSpan sss,
scoped in SeStringDrawParams drawParams = default,
ImGuiId imGuiId = default,
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault)
{
- // Drawing is only valid if done from the main thread anyway, especially with interactivity.
- ThreadSafety.AssertMainThread();
+ // Interactivity is supported only from the main thread.
+ if (!imGuiId.IsEmpty())
+ ThreadSafety.AssertMainThread();
if (drawParams.TargetDrawList is not null && imGuiId)
throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(imGuiId));
- // This also does argument validation for drawParams. Do it here.
- var state = new SeStringDrawState(sss, drawParams, this.colorStackSet, this.splitter);
+ using var cleanup = new DisposeSafety.ScopedFinalizer();
- // Reset and initialize the state.
- this.fragments.Clear();
- this.colorStackSet.Initialize(ref state);
+ ImFont* font = null;
+ if (drawParams.Font.HasValue)
+ font = drawParams.Font.Value;
+ if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null)
+ font = ImGui.GetFont();
+ if (font is null)
+ throw new ArgumentException("Specified font is empty.");
+
+ // This also does argument validation for drawParams. Do it here.
+ // `using var` makes a struct read-only, but we do want to modify it.
+ using var stateStorage = new SeStringDrawState(
+ sss,
+ drawParams,
+ ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes),
+ ThreadSafety.IsMainThread ? this.fragmentsMainThread : [],
+ font);
+ ref var state = ref Unsafe.AsRef(in stateStorage);
// Analyze the provided SeString and break it up to text fragments.
this.CreateTextFragments(ref state);
- var fragmentSpan = CollectionsMarshal.AsSpan(this.fragments);
+ var fragmentSpan = CollectionsMarshal.AsSpan(state.Fragments);
// Calculate size.
var size = Vector2.Zero;
@@ -147,24 +191,17 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
state.SplitDrawList();
- // Handle cases where ImGui.AlignTextToFramePadding has been called.
- var context = ImGui.GetCurrentContext();
- var currLineTextBaseOffset = 0f;
- if (!context.IsNull)
- {
- var currentWindow = context.CurrentWindow;
- if (!currentWindow.IsNull)
- {
- currLineTextBaseOffset = currentWindow.DC.CurrLineTextBaseOffset;
- }
- }
-
var itemSize = size;
- if (currLineTextBaseOffset != 0f)
+ if (drawParams.TargetDrawList is null)
{
- itemSize.Y += 2 * currLineTextBaseOffset;
- foreach (ref var f in fragmentSpan)
- f.Offset += new Vector2(0, currLineTextBaseOffset);
+ // Handle cases where ImGui.AlignTextToFramePadding has been called.
+ var currLineTextBaseOffset = ImGui.GetCurrentContext().CurrentWindow.DC.CurrLineTextBaseOffset;
+ if (currLineTextBaseOffset != 0f)
+ {
+ itemSize.Y += 2 * currLineTextBaseOffset;
+ foreach (ref var f in fragmentSpan)
+ f.Offset += new Vector2(0, currLineTextBaseOffset);
+ }
}
// Draw all text fragments.
@@ -280,15 +317,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return displayRune.Value != 0;
}
- private void ReleaseUnmanagedResources()
- {
- if (this.splitter is not null)
- {
- this.splitter->Destroy();
- this.splitter = null;
- }
- }
-
/// Creates text fragment, taking line and word breaking into account.
/// Draw state.
private void CreateTextFragments(ref SeStringDrawState state)
@@ -391,7 +419,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth;
// Test if the fragment does not fit into the current line and the current line is not empty.
- if (xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows)
+ if (xy.X != 0 && state.Fragments.Count > 0 && !state.Fragments[^1].BreakAfter && overflows)
{
// Introduce break if this is the first time testing the current break unit or the current fragment
// is an entity.
@@ -401,7 +429,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
xy.X = 0;
xy.Y += state.LineHeight;
w = 0;
- CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true;
+ CollectionsMarshal.AsSpan(state.Fragments)[^1].BreakAfter = true;
fragment.Offset = xy;
// Now that the fragment is given its own line, test if it overflows again.
@@ -419,16 +447,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth);
}
}
- else if (this.fragments.Count > 0 && xy.X != 0)
+ else if (state.Fragments.Count > 0 && xy.X != 0)
{
// New fragment fits into the current line, and it has a previous fragment in the same line.
// If the previous fragment ends with a soft hyphen, adjust its width so that the width of its
// trailing soft hyphens are not considered.
- if (this.fragments[^1].EndsWithSoftHyphen)
- xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth;
+ if (state.Fragments[^1].EndsWithSoftHyphen)
+ xy.X += state.Fragments[^1].AdvanceWidthWithoutSoftHyphen - state.Fragments[^1].AdvanceWidth;
// Adjust this fragment's offset from kerning distance.
- xy.X += state.CalculateScaledDistance(this.fragments[^1].LastRune, fragment.FirstRune);
+ xy.X += state.CalculateScaledDistance(state.Fragments[^1].LastRune, fragment.FirstRune);
fragment.Offset = xy;
}
@@ -439,7 +467,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
w = Math.Max(w, xy.X + fragment.VisibleWidth);
xy.X += fragment.AdvanceWidth;
prev = fragment.To;
- this.fragments.Add(fragment);
+ state.Fragments.Add(fragment);
if (fragment.BreakAfter)
{
@@ -491,7 +519,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
if (gfdTextureSrv != 0)
{
state.Draw(
- new ImTextureID(gfdTextureSrv),
+ new(gfdTextureSrv),
offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
size,
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
@@ -528,7 +556,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return;
- static nint GetGfdTextureSrv()
+ static unsafe nint GetGfdTextureSrv()
{
var uim = UIModule.Instance();
if (uim is null)
@@ -553,7 +581,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// Determines a bitmap icon to display for the given SeString payload.
/// Byte span that should include a SeString payload.
/// Icon to display, or if it should not be displayed as an icon.
- private BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan sss)
+ private unsafe BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan sss)
{
var e = new ReadOnlySeStringSpan(sss).GetEnumerator();
if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2)
@@ -710,38 +738,4 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
firstDisplayRune ?? default,
lastNonSoftHyphenRune);
}
-
- /// Represents a text fragment in a SeString span.
- /// Starting byte offset (inclusive) in a SeString.
- /// Ending byte offset (exclusive) in a SeString.
- /// Byte offset of the link that decorates this text fragment, or -1 if none.
- /// Offset in pixels w.r.t. .
- /// Replacement entity, if any.
- /// Visible width of this text fragment. This is the width required to draw everything
- /// without clipping.
- /// Advance width of this text fragment. This is the width required to add to the cursor
- /// to position the next fragment correctly.
- /// Same with , but trimming all the
- /// trailing soft hyphens.
- /// Whether to insert a line break after this text fragment.
- /// Whether this text fragment ends with one or more soft hyphens.
- /// First rune in this text fragment.
- /// Last rune in this text fragment, for the purpose of calculating kerning distance with
- /// the following text fragment in the same line, if any.
- private record struct TextFragment(
- int From,
- int To,
- int Link,
- Vector2 Offset,
- SeStringReplacementEntity Entity,
- float VisibleWidth,
- float AdvanceWidth,
- float AdvanceWidthWithoutSoftHyphen,
- bool BreakAfter,
- bool EndsWithSoftHyphen,
- Rune FirstRune,
- Rune LastRune)
- {
- public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
- }
}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs
new file mode 100644
index 000000000..a64c32109
--- /dev/null
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/TextFragment.cs
@@ -0,0 +1,39 @@
+using System.Numerics;
+using System.Text;
+
+namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
+
+/// Represents a text fragment in a SeString span.
+/// Starting byte offset (inclusive) in a SeString.
+/// Ending byte offset (exclusive) in a SeString.
+/// Byte offset of the link that decorates this text fragment, or -1 if none.
+/// Offset in pixels w.r.t. .
+/// Replacement entity, if any.
+/// Visible width of this text fragment. This is the width required to draw everything
+/// without clipping.
+/// Advance width of this text fragment. This is the width required to add to the cursor
+/// to position the next fragment correctly.
+/// Same with , but trimming all the
+/// trailing soft hyphens.
+/// Whether to insert a line break after this text fragment.
+/// Whether this text fragment ends with one or more soft hyphens.
+/// First rune in this text fragment.
+/// Last rune in this text fragment, for the purpose of calculating kerning distance with
+/// the following text fragment in the same line, if any.
+internal record struct TextFragment(
+ int From,
+ int To,
+ int Link,
+ Vector2 Offset,
+ SeStringReplacementEntity Entity,
+ float VisibleWidth,
+ float AdvanceWidth,
+ float AdvanceWidthWithoutSoftHyphen,
+ bool BreakAfter,
+ bool EndsWithSoftHyphen,
+ Rune FirstRune,
+ Rune LastRune)
+{
+ /// Gets a value indicating whether the fragment ends with a visible soft hyphen.
+ public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
+}
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
index 64a7f3db3..722de1fda 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -6,6 +7,8 @@ using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Utility;
+using Dalamud.Utility;
+
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
@@ -14,51 +17,80 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer;
/// Calculated values from using ImGui styles.
[StructLayout(LayoutKind.Sequential)]
-public unsafe ref struct SeStringDrawState
+public unsafe ref struct SeStringDrawState : IDisposable
{
private static readonly int ChannelCount = Enum.GetValues().Length;
private readonly ImDrawList* drawList;
- private readonly SeStringColorStackSet colorStackSet;
- private readonly ImDrawListSplitter* splitter;
+
+ private ImDrawListSplitter splitter;
/// Initializes a new instance of the struct.
/// Raw SeString byte span.
/// Instance of to initialize from.
/// Instance of to use.
- /// Instance of ImGui Splitter to use.
+ /// Fragments.
+ /// Font to use.
internal SeStringDrawState(
ReadOnlySpan span,
scoped in SeStringDrawParams ssdp,
SeStringColorStackSet colorStackSet,
- ImDrawListSplitter* splitter)
+ List fragments,
+ ImFont* font)
{
- this.colorStackSet = colorStackSet;
- this.splitter = splitter;
- this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
this.Span = span;
+ this.ColorStackSet = colorStackSet;
+ this.Fragments = fragments;
+ this.Font = font;
+
+ if (ssdp.TargetDrawList is null)
+ {
+ if (!ThreadSafety.IsMainThread)
+ {
+ throw new ArgumentException(
+ $"{nameof(ssdp.TargetDrawList)} must be set to render outside the main thread.");
+ }
+
+ this.drawList = ssdp.TargetDrawList ?? ImGui.GetWindowDrawList();
+ this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
+ this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
+ this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
+ this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
+ this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
+ this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
+ this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
+ }
+ else
+ {
+ this.drawList = ssdp.TargetDrawList.Value;
+ this.ScreenOffset = Vector2.Zero;
+ this.FontSize = ssdp.FontSize ?? throw new ArgumentException(
+ $"{nameof(ssdp.FontSize)} must be set to render outside the main thread.");
+ this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue;
+ this.Color = ssdp.Color ?? uint.MaxValue;
+ this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread.
+ this.LinkActiveBackColor = 0; // Interactivity is unused outside the main thread.
+ this.ThemeIndex = ssdp.ThemeIndex ?? 0;
+ }
+
+ this.splitter = default;
this.GetEntity = ssdp.GetEntity;
- this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y));
- this.Font = ssdp.EffectiveFont;
- this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
this.FontSizeScale = this.FontSize / this.Font->FontSize;
this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
- this.WrapWidth = ssdp.WrapWidth ?? ImGui.GetContentRegionAvail().X;
this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
this.Opacity = ssdp.EffectiveOpacity;
this.EdgeOpacity = (ssdp.EdgeStrength ?? 0.25f) * ssdp.EffectiveOpacity;
- this.Color = ssdp.Color ?? ImGui.GetColorU32(ImGuiCol.Text);
this.EdgeColor = ssdp.EdgeColor ?? 0xFF000000;
this.ShadowColor = ssdp.ShadowColor ?? 0xFF000000;
- this.LinkHoverBackColor = ssdp.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered);
- this.LinkActiveBackColor = ssdp.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive);
this.ForceEdgeColor = ssdp.ForceEdgeColor;
- this.ThemeIndex = ssdp.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
this.Bold = ssdp.Bold;
this.Italic = ssdp.Italic;
this.Edge = ssdp.Edge;
this.Shadow = ssdp.Shadow;
+
+ this.ColorStackSet.Initialize(ref this);
+ fragments.Clear();
}
///
@@ -135,7 +167,7 @@ public unsafe ref struct SeStringDrawState
/// Gets a value indicating whether the edge should be drawn.
public readonly bool ShouldDrawEdge =>
- (this.Edge || this.colorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
+ (this.Edge || this.ColorStackSet.HasAdditionalEdgeColor) && this.EdgeColor >= 0x1000000;
/// Gets a value indicating whether the edge should be drawn.
public readonly bool ShouldDrawShadow => this is { Shadow: true, ShadowColor: >= 0x1000000 };
@@ -143,11 +175,21 @@ public unsafe ref struct SeStringDrawState
/// Gets a value indicating whether the edge should be drawn.
public readonly bool ShouldDrawForeground => this is { Color: >= 0x1000000 };
+ /// Gets the color stacks.
+ internal SeStringColorStackSet ColorStackSet { get; }
+
+ /// Gets the text fragments.
+ internal List Fragments { get; }
+
+ ///
+ public void Dispose() =>
+ ImGuiNative.Destroy((ImDrawListSplitter*)Unsafe.AsPointer(ref this.splitter));
+
/// Sets the current channel in the ImGui draw list splitter.
/// Channel to switch to.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public readonly void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
- this.splitter->SetCurrentChannel(this.drawList, (int)channelIndex);
+ public void SetCurrentChannel(SeStringDrawChannel channelIndex) =>
+ this.splitter.SetCurrentChannel(this.drawList, (int)channelIndex);
/// Draws a single texture.
/// ImGui texture ID to draw from.
@@ -216,7 +258,7 @@ public unsafe ref struct SeStringDrawState
/// Draws a single glyph using current styling configurations.
/// Glyph to draw.
/// Offset of the glyph in pixels w.r.t. .
- internal readonly void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
+ internal void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
{
var texId = this.Font->ContainerAtlas->Textures.Ref(g.TextureIndex).TexID;
var xy0 = new Vector2(
@@ -268,7 +310,7 @@ public unsafe ref struct SeStringDrawState
/// Offset of the glyph in pixels w.r.t.
/// .
/// Advance width of the glyph.
- internal readonly void DrawLinkUnderline(Vector2 offset, float advanceWidth)
+ internal void DrawLinkUnderline(Vector2 offset, float advanceWidth)
{
if (this.LinkUnderlineThickness < 1f)
return;
@@ -350,15 +392,15 @@ public unsafe ref struct SeStringDrawState
switch (payload.MacroCode)
{
case MacroCode.Color:
- this.colorStackSet.HandleColorPayload(ref this, payload);
+ this.ColorStackSet.HandleColorPayload(ref this, payload);
return true;
case MacroCode.EdgeColor:
- this.colorStackSet.HandleEdgeColorPayload(ref this, payload);
+ this.ColorStackSet.HandleEdgeColorPayload(ref this, payload);
return true;
case MacroCode.ShadowColor:
- this.colorStackSet.HandleShadowColorPayload(ref this, payload);
+ this.ColorStackSet.HandleShadowColorPayload(ref this, payload);
return true;
case MacroCode.Bold when payload.TryGetExpression(out var e) && e.TryGetUInt(out var u):
@@ -379,11 +421,11 @@ public unsafe ref struct SeStringDrawState
return true;
case MacroCode.ColorType:
- this.colorStackSet.HandleColorTypePayload(ref this, payload);
+ this.ColorStackSet.HandleColorTypePayload(ref this, payload);
return true;
case MacroCode.EdgeColorType:
- this.colorStackSet.HandleEdgeColorTypePayload(ref this, payload);
+ this.ColorStackSet.HandleEdgeColorTypePayload(ref this, payload);
return true;
default:
@@ -393,10 +435,9 @@ public unsafe ref struct SeStringDrawState
/// Splits the draw list.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal readonly void SplitDrawList() =>
- this.splitter->Split(this.drawList, ChannelCount);
+ internal void SplitDrawList() => this.splitter.Split(this.drawList, ChannelCount);
/// Merges the draw list.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal readonly void MergeDrawList() => this.splitter->Merge(this.drawList);
+ internal void MergeDrawList() => this.splitter.Merge(this.drawList);
}
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index d475d36bc..7afe7e709 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -531,6 +531,13 @@ internal class DalamudInterface : IInternalDisposableService
this.creditsDarkeningAnimation.Restart();
}
+ ///
+ public T GetDataWindowWidget() where T : IDataWindowWidget => this.dataWindow.GetWidget();
+
+ /// Sets the data window current widget.
+ /// Widget to set current.
+ public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget;
+
private void OnDraw()
{
this.FrameCount++;
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index ae86958dd..eb0589d59 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -68,7 +68,7 @@ internal class DataWindow : Window, IDisposable
private bool isExcept;
private bool selectionCollapsed;
- private IDataWindowWidget currentWidget;
+
private bool isLoaded;
///
@@ -82,9 +82,12 @@ internal class DataWindow : Window, IDisposable
this.RespectCloseHotkey = false;
this.orderedModules = this.modules.OrderBy(module => module.DisplayName);
- this.currentWidget = this.orderedModules.First();
+ this.CurrentWidget = this.orderedModules.First();
}
+ /// Gets or sets the current widget.
+ public IDataWindowWidget CurrentWidget { get; set; }
+
///
public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose();
@@ -99,6 +102,20 @@ internal class DataWindow : Window, IDisposable
{
}
+ /// Gets the data window widget of the specified type.
+ /// Type of the data window widget to find.
+ /// Found widget.
+ public T GetWidget() where T : IDataWindowWidget
+ {
+ foreach (var m in this.modules)
+ {
+ if (m is T w)
+ return w;
+ }
+
+ throw new ArgumentException($"No widget of type {typeof(T).FullName} found.");
+ }
+
///
/// Set the DataKind dropdown menu.
///
@@ -110,7 +127,7 @@ internal class DataWindow : Window, IDisposable
if (this.modules.FirstOrDefault(module => module.IsWidgetCommand(dataKind)) is { } targetModule)
{
- this.currentWidget = targetModule;
+ this.CurrentWidget = targetModule;
}
else
{
@@ -153,9 +170,9 @@ internal class DataWindow : Window, IDisposable
{
foreach (var widget in this.orderedModules)
{
- if (ImGui.Selectable(widget.DisplayName, this.currentWidget == widget))
+ if (ImGui.Selectable(widget.DisplayName, this.CurrentWidget == widget))
{
- this.currentWidget = widget;
+ this.CurrentWidget = widget;
}
}
@@ -206,9 +223,9 @@ internal class DataWindow : Window, IDisposable
try
{
- if (this.currentWidget is { Ready: true })
+ if (this.CurrentWidget is { Ready: true })
{
- this.currentWidget.Draw();
+ this.CurrentWidget.Draw();
}
else
{
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs
index 91f1af98e..4f5540daf 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs
@@ -1,9 +1,15 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using System.Numerics;
+using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Internal;
+
+using Lumina.Text.ReadOnly;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@@ -87,12 +93,30 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(10f);
for (var i = 0; i < this.icons?.Count; i++)
{
+ if (this.icons[i] == FontAwesomeIcon.None)
+ continue;
+
+ ImGui.AlignTextToFramePadding();
ImGui.Text($"0x{(int)this.icons[i].ToIconChar():X}");
ImGuiHelpers.ScaledRelativeSameLine(50f);
ImGui.Text($"{this.iconNames?[i]}");
ImGuiHelpers.ScaledRelativeSameLine(280f);
ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont);
ImGui.Text(this.icons[i].ToIconString());
+ ImGuiHelpers.ScaledRelativeSameLine(320f);
+ if (this.useFixedWidth
+ ? ImGui.Button($"{(char)this.icons[i]}##FontAwesomeIconButton{i}")
+ : ImGuiComponents.IconButton($"##FontAwesomeIconButton{i}", this.icons[i]))
+ {
+ _ = Service.Get().ShowTextureSaveMenuAsync(
+ this.DisplayName,
+ this.icons[i].ToString(),
+ Task.FromResult(
+ Service.Get().CreateTextureFromSeString(
+ ReadOnlySeString.FromText(this.icons[i].ToIconString()),
+ new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize() })));
+ }
+
ImGui.PopFont();
ImGuiHelpers.ScaledDummy(2f);
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
index 7ff5a63be..0f51e0322 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using System.Text;
+using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Data;
@@ -9,11 +10,13 @@ using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Internal;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel.Sheets;
using Lumina.Text;
+using Lumina.Text.Parse;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
@@ -56,11 +59,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
///
public void Draw()
{
- var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ?? ImGui.GetColorU32(ImGuiCol.Text));
+ var t2 = ImGui.ColorConvertU32ToFloat4(this.style.Color ??= ImGui.GetColorU32(ImGuiCol.Text));
if (ImGui.ColorEdit4("Color", ref t2))
this.style.Color = ImGui.ColorConvertFloat4ToU32(t2);
- t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ?? 0xFF000000u);
+ t2 = ImGui.ColorConvertU32ToFloat4(this.style.EdgeColor ??= 0xFF000000u);
if (ImGui.ColorEdit4("Edge Color", ref t2))
this.style.EdgeColor = ImGui.ColorConvertFloat4ToU32(t2);
@@ -69,27 +72,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (ImGui.Checkbox("Forced"u8, ref t))
this.style.ForceEdgeColor = t;
- t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ?? 0xFF000000u);
- if (ImGui.ColorEdit4("Shadow Color", ref t2))
+ t2 = ImGui.ColorConvertU32ToFloat4(this.style.ShadowColor ??= 0xFF000000u);
+ if (ImGui.ColorEdit4("Shadow Color"u8, ref t2))
this.style.ShadowColor = ImGui.ColorConvertFloat4ToU32(t2);
- t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonHovered));
- if (ImGui.ColorEdit4("Link Hover Color", ref t2))
+ t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkHoverBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonHovered));
+ if (ImGui.ColorEdit4("Link Hover Color"u8, ref t2))
this.style.LinkHoverBackColor = ImGui.ColorConvertFloat4ToU32(t2);
- t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ?? ImGui.GetColorU32(ImGuiCol.ButtonActive));
- if (ImGui.ColorEdit4("Link Active Color", ref t2))
+ t2 = ImGui.ColorConvertU32ToFloat4(this.style.LinkActiveBackColor ??= ImGui.GetColorU32(ImGuiCol.ButtonActive));
+ if (ImGui.ColorEdit4("Link Active Color"u8, ref t2))
this.style.LinkActiveBackColor = ImGui.ColorConvertFloat4ToU32(t2);
- var t3 = this.style.LineHeight ?? 1f;
+ var t3 = this.style.LineHeight ??= 1f;
if (ImGui.DragFloat("Line Height"u8, ref t3, 0.01f, 0.4f, 3f, "%.02f"))
this.style.LineHeight = t3;
- t3 = this.style.Opacity ?? ImGui.GetStyle().Alpha;
+ t3 = this.style.Opacity ??= 1f;
if (ImGui.DragFloat("Opacity"u8, ref t3, 0.005f, 0f, 1f, "%.02f"))
this.style.Opacity = t3;
- t3 = this.style.EdgeStrength ?? 0.25f;
+ t3 = this.style.EdgeStrength ??= 0.25f;
if (ImGui.DragFloat("Edge Strength"u8, ref t3, 0.005f, 0f, 1f, "%.02f"))
this.style.EdgeStrength = t3;
@@ -240,6 +243,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
Service.Get().CompileAndCache(this.testString).Data.Span));
}
+ ImGui.SameLine();
+
+ if (ImGui.Button("Copy as Image"))
+ {
+ _ = Service.Get().ShowTextureSaveMenuAsync(
+ this.DisplayName,
+ $"From {nameof(SeStringRendererTestWidget)}",
+ Task.FromResult(
+ Service.Get().CreateTextureFromSeString(
+ ReadOnlySeString.FromMacroString(
+ this.testString,
+ new(ExceptionMode: MacroStringParseExceptionMode.EmbedError)),
+ this.style with
+ {
+ Font = ImGui.GetFont(),
+ FontSize = ImGui.GetFontSize(),
+ WrapWidth = ImGui.GetContentRegionAvail().X,
+ ThemeIndex = AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType,
+ })));
+ }
+
ImGuiHelpers.ScaledDummy(3);
ImGuiHelpers.CompileSeStringWrapped(
"Optional features implemented for the following test input:
" +
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
index 52fa0e822..3416a2506 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
@@ -306,12 +306,12 @@ internal class TexWidget : IDataWindowWidget
pres->Release();
ImGui.Text($"RC: Resource({rcres})/View({rcsrv})");
- ImGui.Text(source.ToString());
+ ImGui.Text($"{source.Width} x {source.Height} | {source}");
}
else
{
- ImGui.Text("RC: -"u8);
- ImGui.Text(" "u8);
+ ImGui.Text("RC: -");
+ ImGui.Text(string.Empty);
}
}
@@ -342,6 +342,10 @@ internal class TexWidget : IDataWindowWidget
runLater?.Invoke();
}
+ /// Adds a texture wrap for debug display purposes.
+ /// Task returning a texture.
+ public void AddTexture(Task textureTask) => this.addedTextures.Add(new(Api10: textureTask));
+
private unsafe void DrawBlame(List allBlames)
{
var im = Service.Get();
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
index 2853aa4d2..be2f5a742 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System.Threading;
+using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
@@ -33,10 +34,22 @@ public interface IFontHandle : IDisposable
///
///
/// Use directly if you want to keep the current ImGui font if the font is not ready.
- /// Alternatively, use to wait for this property to become true.
+ /// Alternatively, use to wait for this property to become true.
///
bool Available { get; }
+ ///
+ /// Attempts to lock the fully constructed instance of corresponding to the this
+ /// , for use in any thread.
+ /// Modification of the font will exhibit undefined behavior if some other thread also uses the font.
+ ///
+ /// The error message, if any.
+ ///
+ /// An instance of that must be disposed after use on success;
+ /// null with populated on failure.
+ ///
+ ILockedImFont? TryLock(out string? errorMessage);
+
///
/// Locks the fully constructed instance of corresponding to the this
/// , for use in any thread.
@@ -92,4 +105,11 @@ public interface IFontHandle : IDisposable
///
/// A task containing this .
Task WaitAsync();
+
+ ///
+ /// Waits for to become true.
+ ///
+ /// The cancellation token.
+ /// A task containing this .
+ Task WaitAsync(CancellationToken cancellationToken);
}
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
index 1fdaf4596..98a823deb 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
@@ -238,12 +238,17 @@ internal abstract class FontHandle : IFontHandle
}
///
- public Task WaitAsync()
+ public Task WaitAsync() => this.WaitAsync(CancellationToken.None);
+
+ ///
+ public Task WaitAsync(CancellationToken cancellationToken)
{
if (this.Available)
return Task.FromResult(this);
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ cancellationToken.Register(() => tcs.TrySetCanceled());
+
this.ImFontChanged += OnImFontChanged;
this.Disposed += OnDisposed;
if (this.Available)
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs
new file mode 100644
index 000000000..3e90ae3ea
--- /dev/null
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromSeString.cs
@@ -0,0 +1,35 @@
+using Dalamud.Interface.ImGuiSeStringRenderer;
+using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
+using Dalamud.Interface.Textures.TextureWraps;
+using Dalamud.Utility;
+
+namespace Dalamud.Interface.Textures.Internal;
+
+/// Service responsible for loading and disposing ImGui texture wraps.
+internal sealed partial class TextureManager
+{
+ [ServiceManager.ServiceDependency]
+ private readonly SeStringRenderer seStringRenderer = Service.Get();
+
+ ///
+ public IDalamudTextureWrap CreateTextureFromSeString(
+ ReadOnlySpan text,
+ scoped in SeStringDrawParams drawParams = default,
+ string? debugName = null)
+ {
+ ThreadSafety.AssertMainThread();
+ using var dd = this.seStringRenderer.CreateDrawData(text, drawParams);
+ var texture = this.CreateDrawListTexture(debugName ?? nameof(this.CreateTextureFromSeString));
+ try
+ {
+ texture.Size = dd.Data.DisplaySize;
+ texture.Draw(dd.DataPtr);
+ return texture;
+ }
+ catch
+ {
+ texture.Dispose();
+ throw;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs
index 059c716ce..d0f0d8c07 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManager.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
+using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps;
@@ -248,7 +249,7 @@ internal sealed partial class TextureManager
usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC;
else
usage = D3D11_USAGE.D3D11_USAGE_DEFAULT;
-
+
using var texture = this.device.CreateTexture2D(
new()
{
diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
index 9b0fa0943..ac6de7dd7 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs
@@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
+using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.IoC;
@@ -283,6 +284,18 @@ internal sealed class TextureManagerPluginScoped
return textureWrap;
}
+ ///
+ public IDalamudTextureWrap CreateTextureFromSeString(
+ ReadOnlySpan text,
+ scoped in SeStringDrawParams drawParams = default,
+ string? debugName = null)
+ {
+ var manager = this.ManagerOrThrow;
+ var textureWrap = manager.CreateTextureFromSeString(text, drawParams, debugName);
+ manager.Blame(textureWrap, this.plugin);
+ return textureWrap;
+ }
+
///
public IEnumerable GetSupportedImageDecoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();
diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs
index b870e7a94..355b5d571 100644
--- a/Dalamud/Interface/UiBuilder.cs
+++ b/Dalamud/Interface/UiBuilder.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
+using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
@@ -657,13 +658,14 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
FontAtlasAutoRebuildMode autoRebuildMode,
bool isGlobalScaled = true,
string? debugName = null) =>
- this.scopedFinalizer.Add(Service
- .Get()
- .CreateFontAtlas(
- this.namespaceName + ":" + (debugName ?? "custom"),
- autoRebuildMode,
- isGlobalScaled,
- this.plugin));
+ this.scopedFinalizer.Add(
+ Service
+ .Get()
+ .CreateFontAtlas(
+ this.namespaceName + ":" + (debugName ?? "custom"),
+ autoRebuildMode,
+ isGlobalScaled,
+ this.plugin));
///
/// Unregister the UiBuilder. Do not call this in plugin code.
@@ -825,6 +827,15 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
// Note: do not dispose w; we do not own it
}
+ public ILockedImFont? TryLock(out string? errorMessage)
+ {
+ if (this.wrapped is { } w)
+ return w.TryLock(out errorMessage);
+
+ errorMessage = nameof(ObjectDisposedException);
+ return null;
+ }
+
public ILockedImFont Lock() =>
this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper));
@@ -833,7 +844,13 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
public void Pop() => this.WrappedNotDisposed.Pop();
public Task WaitAsync() =>
- this.WrappedNotDisposed.WaitAsync().ContinueWith(_ => (IFontHandle)this);
+ this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this)
+ ?? Task.FromException(new ObjectDisposedException(nameof(FontHandleWrapper)));
+
+ public Task WaitAsync(CancellationToken cancellationToken) =>
+ this.wrapped?.WaitAsync(cancellationToken)
+ .ContinueWith(_ => (IFontHandle)this, cancellationToken)
+ ?? Task.FromException(new ObjectDisposedException(nameof(FontHandleWrapper)));
public override string ToString() =>
$"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})";
diff --git a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
new file mode 100644
index 000000000..112fda8a8
--- /dev/null
+++ b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
@@ -0,0 +1,112 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Dalamud.Bindings.ImGui;
+
+namespace Dalamud.Interface.Utility;
+
+/// Wrapper aroundx containing one .
+public unsafe struct BufferBackedImDrawData : IDisposable
+{
+ private nint buffer;
+
+ /// Initializes a new instance of the struct.
+ /// Address of buffer to use.
+ private BufferBackedImDrawData(nint buffer) => this.buffer = buffer;
+
+ /// Gets the stored in this buffer.
+ public readonly ref ImDrawData Data => ref ((DataStruct*)this.buffer)->Data;
+
+ /// Gets the stored in this buffer.
+ public readonly ImDrawDataPtr DataPtr => new((ImDrawData*)Unsafe.AsPointer(ref this.Data));
+
+ /// Gets the stored in this buffer.
+ public readonly ref ImDrawList List => ref ((DataStruct*)this.buffer)->List;
+
+ /// Gets the stored in this buffer.
+ public readonly ImDrawListPtr ListPtr => new((ImDrawList*)Unsafe.AsPointer(ref this.List));
+
+ /// Creates a new instance of .
+ /// A new instance of .
+ public static BufferBackedImDrawData Create()
+ {
+ if (ImGui.GetCurrentContext().IsNull || ImGui.GetIO().FontDefault.Handle is null)
+ throw new("ImGui is not ready");
+
+ var res = new BufferBackedImDrawData(Marshal.AllocHGlobal(sizeof(DataStruct)));
+ var ds = (DataStruct*)res.buffer;
+ *ds = default;
+
+ var atlas = ImGui.GetIO().Fonts;
+ ref var atlasTail = ref ImFontAtlasTailReal.From(atlas);
+ ds->SharedData = *ImGui.GetDrawListSharedData().Handle;
+ ds->SharedData.TexIdCommon = atlas.Textures[atlasTail.TextureIndexCommon].TexID;
+ ds->SharedData.TexUvWhitePixel = atlas.TexUvWhitePixel;
+ ds->SharedData.TexUvLines = (Vector4*)Unsafe.AsPointer(ref atlas.TexUvLines[0]);
+ ds->SharedData.Font = ImGui.GetIO().FontDefault;
+ ds->SharedData.FontSize = ds->SharedData.Font->FontSize;
+ ds->SharedData.ClipRectFullscreen = new(
+ float.NegativeInfinity,
+ float.NegativeInfinity,
+ float.PositiveInfinity,
+ float.PositiveInfinity);
+
+ ds->List.Data = &ds->SharedData;
+ ds->ListPtr = &ds->List;
+ ds->Data.CmdLists = &ds->ListPtr;
+ ds->Data.CmdListsCount = 1;
+ ds->Data.FramebufferScale = Vector2.One;
+
+ res.ListPtr._ResetForNewFrame();
+ res.ListPtr.PushClipRectFullScreen();
+ res.ListPtr.PushTextureID(new(atlasTail.TextureIndexCommon));
+ return res;
+ }
+
+ /// Updates the statistics information stored in from .
+ public readonly void UpdateDrawDataStatistics()
+ {
+ this.Data.TotalIdxCount = this.List.IdxBuffer.Size;
+ this.Data.TotalVtxCount = this.List.VtxBuffer.Size;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (this.buffer != 0)
+ {
+ this.ListPtr._ClearFreeMemory();
+ Marshal.FreeHGlobal(this.buffer);
+ this.buffer = 0;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct DataStruct
+ {
+ public ImDrawData Data;
+ public ImDrawList* ListPtr;
+ public ImDrawList List;
+ public ImDrawListSharedData SharedData;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct ImFontAtlasTailReal
+ {
+ /// Index of texture containing the below.
+ public int TextureIndexCommon;
+
+ /// Custom texture rectangle ID for both of the below.
+ public int PackIdCommon;
+
+ /// Custom texture rectangle for white pixel and mouse cursors.
+ public ImFontAtlasCustomRect RectMouseCursors;
+
+ /// Custom texture rectangle for baked anti-aliased lines.
+ public ImFontAtlasCustomRect RectLines;
+
+ public static ref ImFontAtlasTailReal From(ImFontAtlasPtr fontAtlasPtr) =>
+ ref *(ImFontAtlasTailReal*)(&fontAtlasPtr.Handle->FontBuilderFlags + sizeof(uint));
+ }
+}
diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs
index 27cb3596c..98159c1bc 100644
--- a/Dalamud/Interface/Utility/ImGuiHelpers.cs
+++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs
@@ -234,6 +234,15 @@ public static partial class ImGuiHelpers
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
Service.Get().CompileAndDrawWrapped(text, style, imGuiId, buttonFlags);
+ /// Creates a draw data that will draw the given SeString onto it.
+ /// SeString to render.
+ /// Initial rendering style.
+ /// A new self-contained draw data.
+ public static BufferBackedImDrawData CreateDrawData(
+ ReadOnlySpan sss,
+ scoped in SeStringDrawParams style = default) =>
+ Service.Get().CreateDrawData(sss, style);
+
///
/// Write unformatted text wrapped.
///
diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
index 86435e8c1..4a0137c88 100644
--- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
+++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
@@ -6,10 +6,12 @@ using System.Text;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
+using Dalamud.Game;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
+using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Serilog;
@@ -33,6 +35,14 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
this.interfaceManager.Draw += this.InterfaceManagerOnDraw;
}
+ private enum ContextMenuActionType
+ {
+ None,
+ SaveAsFile,
+ CopyToClipboard,
+ SendToTexWidget,
+ }
+
///
void IInternalDisposableService.DisposeService() => this.interfaceManager.Draw -= this.InterfaceManagerOnDraw;
@@ -66,15 +76,16 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
var textureManager = await Service.GetAsync();
var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.Handle.Handle:X}";
+ ContextMenuActionType action;
BitmapCodecInfo? encoder;
{
var first = true;
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
- var tcs = new TaskCompletionSource(
+ var tcs = new TaskCompletionSource<(ContextMenuActionType Action, BitmapCodecInfo? Codec)>(
TaskCreationOptions.RunContinuationsAsynchronously);
Service.Get().Draw += DrawChoices;
- encoder = await tcs.Task;
+ (action, encoder) = await tcs.Task;
[SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "This shall not escape")]
void DrawChoices()
@@ -98,13 +109,20 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
}
if (ImGui.Selectable("Copy"u8))
- tcs.TrySetResult(null);
+ tcs.TrySetResult((ContextMenuActionType.CopyToClipboard, null));
+ if (ImGui.Selectable("Send to TexWidget"u8))
+ tcs.TrySetResult((ContextMenuActionType.SendToTexWidget, null));
+
+ ImGui.Separator();
+
foreach (var encoder2 in encoders)
{
if (ImGui.Selectable(encoder2.Name))
- tcs.TrySetResult(encoder2);
+ tcs.TrySetResult((ContextMenuActionType.SaveAsFile, encoder2));
}
+ ImGui.Separator();
+
const float previewImageWidth = 320;
var size = textureWrap.Size;
if (size.X > previewImageWidth)
@@ -120,50 +138,68 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
}
}
- if (encoder is null)
+ switch (action)
{
- isCopy = true;
- await textureManager.CopyToClipboardAsync(textureWrap, name, true);
- }
- else
- {
- var props = new Dictionary();
- if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
- props["CompressionQuality"] = 1.0f;
- else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
- encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
- encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
- props["ImageQuality"] = 1.0f;
+ case ContextMenuActionType.CopyToClipboard:
+ isCopy = true;
+ await textureManager.CopyToClipboardAsync(textureWrap, name, true);
+ break;
- var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- this.fileDialogManager.SaveFileDialog(
- "Save texture...",
- $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
- name + encoder.Extensions.First(),
- encoder.Extensions.First(),
- (ok, path2) =>
- {
- if (!ok)
- tcs.SetCanceled();
- else
- tcs.SetResult(path2);
- });
- var path = await tcs.Task.ConfigureAwait(false);
-
- await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
-
- var notif = Service.Get().AddNotification(
- new()
- {
- Content = $"File saved to: {path}",
- Title = initiatorName,
- Type = NotificationType.Success,
- });
- notif.Click += n =>
+ case ContextMenuActionType.SendToTexWidget:
{
- Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
- n.Notification.DismissNow();
- };
+ var framework = await Service.GetAsync();
+ var dalamudInterface = await Service.GetAsync();
+ await framework.RunOnFrameworkThread(
+ () =>
+ {
+ var texWidget = dalamudInterface.GetDataWindowWidget();
+ dalamudInterface.SetDataWindowWidget(texWidget);
+ texWidget.AddTexture(Task.FromResult(textureWrap.CreateWrapSharingLowLevelResource()));
+ });
+ break;
+ }
+
+ case ContextMenuActionType.SaveAsFile when encoder is not null:
+ {
+ var props = new Dictionary();
+ if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
+ props["CompressionQuality"] = 1.0f;
+ else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
+ encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
+ encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
+ props["ImageQuality"] = 1.0f;
+
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ this.fileDialogManager.SaveFileDialog(
+ "Save texture...",
+ $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
+ name + encoder.Extensions.First(),
+ encoder.Extensions.First(),
+ (ok, path2) =>
+ {
+ if (!ok)
+ tcs.SetCanceled();
+ else
+ tcs.SetResult(path2);
+ });
+ var path = await tcs.Task.ConfigureAwait(false);
+
+ await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props);
+
+ var notif = Service.Get().AddNotification(
+ new()
+ {
+ Content = $"File saved to: {path}",
+ Title = initiatorName,
+ Type = NotificationType.Success,
+ });
+ notif.Click += n =>
+ {
+ Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
+ n.Notification.DismissNow();
+ };
+ break;
+ }
}
}
catch (Exception e)
diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs
index a8ad76995..9c499d3f5 100644
--- a/Dalamud/Plugin/Services/ITextureProvider.cs
+++ b/Dalamud/Plugin/Services/ITextureProvider.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
+using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
@@ -186,6 +187,17 @@ public interface ITextureProvider
string? debugName = null,
CancellationToken cancellationToken = default);
+ /// Creates a texture by drawing a SeString onto it.
+ /// SeString to render.
+ /// Parameters for drawing.
+ /// Name for debug display purposes.
+ /// The new texture.
+ /// Can be only be used from the main thread.
+ public IDalamudTextureWrap CreateTextureFromSeString(
+ ReadOnlySpan text,
+ scoped in SeStringDrawParams drawParams = default,
+ string? debugName = null);
+
/// Gets the supported bitmap decoders.
/// The supported bitmap decoders.
///
From 40e63f2d9a7e015f1c7659f714d6aff1c3397918 Mon Sep 17 00:00:00 2001
From: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Date: Mon, 11 Aug 2025 00:44:02 +0900
Subject: [PATCH 002/201] Enable viewport alpha
---
Dalamud/Interface/Windowing/Window.cs | 21 +++++++++------------
lib/cimgui | 2 +-
2 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index d302552f5..a6b5e0801 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -449,11 +449,8 @@ public abstract class Window
}
// Not supported yet on non-main viewports
- if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) &&
- ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
+ if (this.internalIsClickthrough && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
{
- this.internalAlpha = null;
- this.internalIsPinned = false;
this.internalIsClickthrough = false;
this.presetDirty = true;
}
@@ -482,11 +479,6 @@ public abstract class Window
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
{
- var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
-
- if (!isAvailable)
- ImGui.BeginDisabled();
-
if (this.internalIsClickthrough)
ImGui.BeginDisabled();
@@ -506,6 +498,11 @@ public abstract class Window
if (this.internalIsClickthrough)
ImGui.EndDisabled();
+ var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
+
+ if (!isAvailable)
+ ImGui.BeginDisabled();
+
if (this.AllowClickthrough)
{
if (ImGui.Checkbox(
@@ -519,6 +516,9 @@ public abstract class Window
Loc.Localize("WindowSystemContextActionClickthroughHint", "Clickthrough windows will not receive mouse input, move or resize. They are completely inert."));
}
+ if (!isAvailable)
+ ImGui.EndDisabled();
+
var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
100f))
@@ -547,9 +547,6 @@ public abstract class Window
"These features are only available if this window is inside the game window."));
}
- if (!isAvailable)
- ImGui.EndDisabled();
-
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
printWindow = true;
diff --git a/lib/cimgui b/lib/cimgui
index 27c8565f6..68cce5e21 160000
--- a/lib/cimgui
+++ b/lib/cimgui
@@ -1 +1 @@
-Subproject commit 27c8565f631b004c3266373890e41ecc627f775b
+Subproject commit 68cce5e2185948612eb80d981d4001b9737c32cf
From e5451c37af8dc2f9b24c6bee35c2416e6a65b3a0 Mon Sep 17 00:00:00 2001
From: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Date: Tue, 12 Aug 2025 16:18:49 +0900
Subject: [PATCH 003/201] Update InputHandler to match changes in
imgui_impl_win32.cpp
---
.../InputHandler/Win32InputHandler.cs | 255 ++++++++++--------
.../Internal/Windows/TitleScreenMenuWindow.cs | 3 +-
imgui/Dalamud.Bindings.ImGui/ImVector.cs | 237 ++++++++++------
3 files changed, 308 insertions(+), 187 deletions(-)
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
index 596df4c67..62e254a1a 100644
--- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
@@ -34,11 +34,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private readonly HCURSOR[] cursors;
private readonly WndProcDelegate wndProcDelegate;
- private readonly bool[] imguiMouseIsDown;
private readonly nint platformNamePtr;
private ViewportHandler viewportHandler;
+ private int mouseButtonsDown;
+ private bool mouseTracked;
private long lastTime;
private nint iniPathPtr;
@@ -64,7 +65,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors |
ImGuiBackendFlags.HasSetMousePos |
ImGuiBackendFlags.RendererHasViewports |
- ImGuiBackendFlags.PlatformHasViewports;
+ ImGuiBackendFlags.PlatformHasViewports |
+ ImGuiBackendFlags.HasMouseHoveredViewport;
this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#");
io.Handle->BackendPlatformName = (byte*)this.platformNamePtr;
@@ -74,8 +76,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
this.viewportHandler = new(this);
- this.imguiMouseIsDown = new bool[5];
-
this.cursors = new HCURSOR[9];
this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW);
this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM);
@@ -95,8 +95,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam);
- private delegate BOOL MonitorEnumProcDelegate(HMONITOR monitor, HDC hdc, RECT* rect, LPARAM lparam);
-
///
public bool UpdateCursor { get; set; } = true;
@@ -155,6 +153,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
public void NewFrame(int targetWidth, int targetHeight)
{
var io = ImGui.GetIO();
+ var focusedWindow = GetForegroundWindow();
io.DisplaySize.X = targetWidth;
io.DisplaySize.Y = targetHeight;
@@ -168,9 +167,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors();
- this.UpdateMousePos();
+ this.UpdateMouseData(focusedWindow);
- this.ProcessKeyEventsWorkarounds();
+ this.ProcessKeyEventsWorkarounds(focusedWindow);
// TODO: need to figure out some way to unify all this
// The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues
@@ -224,6 +223,40 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
switch (msg)
{
+ case WM.WM_MOUSEMOVE:
+ {
+ if (!this.mouseTracked)
+ {
+ var tme = new TRACKMOUSEEVENT
+ {
+ cbSize = (uint)sizeof(TRACKMOUSEEVENT),
+ dwFlags = TME.TME_LEAVE,
+ hwndTrack = hWndCurrent,
+ };
+ this.mouseTracked = TrackMouseEvent(&tme);
+ }
+
+ var mousePos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
+ if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
+ ClientToScreen(hWndCurrent, &mousePos);
+ io.AddMousePosEvent(mousePos.x, mousePos.y);
+ break;
+ }
+
+ case WM.WM_MOUSELEAVE:
+ {
+ this.mouseTracked = false;
+ var mouseScreenPos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
+ ClientToScreen(hWndCurrent, &mouseScreenPos);
+ if (this.ViewportFromPoint(mouseScreenPos).IsNull)
+ {
+ var fltMax = ImGuiNative.GETFLTMAX();
+ io.AddMousePosEvent(-fltMax, -fltMax);
+ }
+
+ break;
+ }
+
case WM.WM_LBUTTONDOWN:
case WM.WM_LBUTTONDBLCLK:
case WM.WM_RBUTTONDOWN:
@@ -236,11 +269,10 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
var button = GetButton(msg, wParam);
if (io.WantCaptureMouse)
{
- if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero)
+ if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero)
SetCapture(hWndCurrent);
-
- io.MouseDown[button] = true;
- this.imguiMouseIsDown[button] = true;
+ this.mouseButtonsDown |= 1 << button;
+ io.AddMouseButtonEvent(button, true);
return default(LRESULT);
}
@@ -256,13 +288,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_XBUTTONUP:
{
var button = GetButton(msg, wParam);
- if (io.WantCaptureMouse && this.imguiMouseIsDown[button])
+ if (io.WantCaptureMouse)
{
- if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
+ this.mouseButtonsDown &= ~(1 << button);
+ if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent)
ReleaseCapture();
-
- io.MouseDown[button] = false;
- this.imguiMouseIsDown[button] = false;
+ io.AddMouseButtonEvent(button, false);
return default(LRESULT);
}
@@ -272,7 +303,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEWHEEL:
if (io.WantCaptureMouse)
{
- io.MouseWheel += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
+ io.AddMouseWheelEvent(0, GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
return default(LRESULT);
}
@@ -280,7 +311,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEHWHEEL:
if (io.WantCaptureMouse)
{
- io.MouseWheelH += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
+ io.AddMouseWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0);
return default(LRESULT);
}
@@ -374,68 +405,86 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors();
break;
- case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
- if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
- ReleaseCapture();
+ case WM.WM_SETFOCUS when hWndCurrent == this.hWnd:
+ io.AddFocusEvent(true);
+ break;
- ImGui.GetIO().WantCaptureMouse = false;
- ImGui.ClearWindowFocus();
+ case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
+ io.AddFocusEvent(false);
+ // if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
+ // ReleaseCapture();
+ //
+ // ImGui.GetIO().WantCaptureMouse = false;
+ // ImGui.ClearWindowFocus();
break;
}
return null;
}
- private void UpdateMousePos()
+ private void UpdateMouseData(HWND focusedWindow)
{
var io = ImGui.GetIO();
- var pt = default(POINT);
- // Depending on if Viewports are enabled, we have to change how we process
- // the cursor position. If viewports are enabled, we pass the absolute cursor
- // position to ImGui. Otherwise, we use the old method of passing client-local
- // mouse position to ImGui.
- if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
+ var mouseScreenPos = default(POINT);
+ var hasMouseScreenPos = GetCursorPos(&mouseScreenPos) != 0;
+
+ var isAppFocused =
+ focusedWindow != default
+ && (focusedWindow == this.hWnd
+ || IsChild(focusedWindow, this.hWnd)
+ || !ImGui.FindViewportByPlatformHandle(focusedWindow).IsNull);
+
+ if (isAppFocused)
{
+ // (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user)
+ // When multi-viewports are enabled, all Dear ImGui positions are same as OS positions.
if (io.WantSetMousePos)
{
- SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y);
+ var pos = new POINT((int)io.MousePos.X, (int)io.MousePos.Y);
+ if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
+ ClientToScreen(this.hWnd, &pos);
+ SetCursorPos(pos.x, pos.y);
}
- if (GetCursorPos(&pt))
+ // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
+ if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
{
- io.MousePos.X = pt.x;
- io.MousePos.Y = pt.y;
- }
- else
- {
- io.MousePos.X = float.MinValue;
- io.MousePos.Y = float.MinValue;
+ // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window)
+ // (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.)
+ // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor)
+ // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.)
+ var mousePos = mouseScreenPos;
+ if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
+ ClientToScreen(focusedWindow, &mousePos);
+ io.AddMousePosEvent(mousePos.x, mousePos.y);
}
}
+
+ // (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering.
+ // If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic.
+ // - [X] Win32 backend correctly ignore viewports with the _NoInputs flag (here using ::WindowFromPoint with WM_NCHITTEST + HTTRANSPARENT in WndProc does that)
+ // Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window
+ // for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported
+ // by the backend, and use its flawed heuristic to guess the viewport behind.
+ // - [X] Win32 backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target).
+ if (hasMouseScreenPos)
+ {
+ var viewport = this.ViewportFromPoint(mouseScreenPos);
+ io.AddMouseViewportEvent(!viewport.IsNull ? viewport.ID : 0u);
+ }
else
{
- if (io.WantSetMousePos)
- {
- pt.x = (int)io.MousePos.X;
- pt.y = (int)io.MousePos.Y;
- ClientToScreen(this.hWnd, &pt);
- SetCursorPos(pt.x, pt.y);
- }
-
- if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt))
- {
- io.MousePos.X = pt.x;
- io.MousePos.Y = pt.y;
- }
- else
- {
- io.MousePos.X = float.MinValue;
- io.MousePos.Y = float.MinValue;
- }
+ io.AddMouseViewportEvent(0);
}
}
+ private ImGuiViewportPtr ViewportFromPoint(POINT mouseScreenPos)
+ {
+ var hoveredHwnd = WindowFromPoint(mouseScreenPos);
+ return hoveredHwnd != default ? ImGui.FindViewportByPlatformHandle(hoveredHwnd) : default;
+ }
+
private bool UpdateMouseCursor()
{
var io = ImGui.GetIO();
@@ -451,7 +500,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return true;
}
- private void ProcessKeyEventsWorkarounds()
+ private void ProcessKeyEventsWorkarounds(HWND focusedWindow)
{
// Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT))
@@ -480,7 +529,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{
// See: https://github.com/goatcorp/ImGuiScene/pull/13
// > GetForegroundWindow from winuser.h is a surprisingly expensive function.
- var isForeground = GetForegroundWindow() == this.hWnd;
+ var isForeground = focusedWindow == this.hWnd;
for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++)
{
// Skip raising modifier keys if the game is focused.
@@ -646,14 +695,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return;
var pio = ImGui.GetPlatformIO();
-
- if (ImGui.GetPlatformIO().Handle->Monitors.Data != null)
- {
- // We allocated the platform monitor data in OnUpdateMonitors ourselves,
- // so we have to free it ourselves to ImGui doesn't try to, or else it will crash
- Marshal.FreeHGlobal(new IntPtr(ImGui.GetPlatformIO().Handle->Monitors.Data));
- ImGui.GetPlatformIO().Handle->Monitors = default;
- }
+ ImGui.GetPlatformIO().Handle->Monitors.Free();
fixed (char* windowClassNamePtr = WindowClassName)
{
@@ -693,59 +735,50 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
// Here we use a manual ImVector overload, free the existing monitor data,
// and allocate our own, as we are responsible for telling ImGui about monitors
var pio = ImGui.GetPlatformIO();
- var numMonitors = GetSystemMetrics(SM.SM_CMONITORS);
- var data = Marshal.AllocHGlobal(Marshal.SizeOf() * numMonitors);
- if (pio.Handle->Monitors.Data != null)
- Marshal.FreeHGlobal(new IntPtr(pio.Handle->Monitors.Data));
- pio.Handle->Monitors = new(numMonitors, numMonitors, (ImGuiPlatformMonitor*)data.ToPointer());
+ pio.Handle->Monitors.Resize(0);
- // ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO();
- // Marshal.FreeHGlobal(platformIO.Handle->Monitors.Data);
- // int numMonitors = GetSystemMetrics(SystemMetric.SM_CMONITORS);
- // nint data = Marshal.AllocHGlobal(Marshal.SizeOf() * numMonitors);
- // platformIO.Handle->Monitors = new ImVector(numMonitors, numMonitors, data);
-
- var monitorIndex = -1;
- var enumfn = new MonitorEnumProcDelegate(
- (hMonitor, _, _, _) =>
- {
- monitorIndex++;
- var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
- if (!GetMonitorInfoW(hMonitor, &info))
- return true;
-
- var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
- var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
- var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
- var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
- // Give ImGui the info for this display
-
- ref var imMonitor = ref ImGui.GetPlatformIO().Monitors.Ref(monitorIndex);
- imMonitor.MainPos = monitorLt;
- imMonitor.MainSize = monitorRb - monitorLt;
- imMonitor.WorkPos = workLt;
- imMonitor.WorkSize = workRb - workLt;
- imMonitor.DpiScale = 1f;
- return true;
- });
- EnumDisplayMonitors(
- default,
- null,
- (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(enumfn),
- default);
+ EnumDisplayMonitors(default, null, &EnumDisplayMonitorsCallback, default);
Log.Information("Monitors set up!");
- for (var i = 0; i < numMonitors; i++)
+ foreach (ref var monitor in pio.Handle->Monitors)
{
- var monitor = pio.Handle->Monitors[i];
Log.Information(
- "Monitor {Index}: {MainPos} {MainSize} {WorkPos} {WorkSize}",
- i,
+ "Monitor: {MainPos} {MainSize} {WorkPos} {WorkSize}",
monitor.MainPos,
monitor.MainSize,
monitor.WorkPos,
monitor.WorkSize);
}
+
+ return;
+
+ [UnmanagedCallersOnly]
+ static BOOL EnumDisplayMonitorsCallback(HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM lParam)
+ {
+ var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
+ if (!GetMonitorInfoW(hMonitor, &info))
+ return true;
+
+ var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
+ var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
+ var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
+ var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
+
+ // Give ImGui the info for this display
+ var imMonitor = new ImGuiPlatformMonitor
+ {
+ MainPos = monitorLt,
+ MainSize = monitorRb - monitorLt,
+ WorkPos = workLt,
+ WorkSize = workRb - workLt,
+ DpiScale = 1f,
+ };
+ if ((info.dwFlags & MONITORINFOF_PRIMARY) != 0)
+ ImGui.GetPlatformIO().Monitors.PushFront(imMonitor);
+ else
+ ImGui.GetPlatformIO().Monitors.PushBack(imMonitor);
+ return true;
+ }
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
index e3eb22a04..69cdc4d28 100644
--- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
@@ -86,7 +86,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
: base(
"TitleScreenMenuOverlay",
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar |
- ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus)
+ ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus |
+ ImGuiWindowFlags.NoDocking)
{
this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true);
diff --git a/imgui/Dalamud.Bindings.ImGui/ImVector.cs b/imgui/Dalamud.Bindings.ImGui/ImVector.cs
index 9a10c1d6b..67e450193 100644
--- a/imgui/Dalamud.Bindings.ImGui/ImVector.cs
+++ b/imgui/Dalamud.Bindings.ImGui/ImVector.cs
@@ -1,7 +1,12 @@
-using System.Runtime.CompilerServices;
+using System.Collections;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace Dalamud.Bindings.ImGui;
+///
+/// A structure representing a dynamic array for unmanaged types.
+///
public unsafe struct ImVector
{
public readonly int Size;
@@ -15,23 +20,23 @@ public unsafe struct ImVector
Data = data;
}
- public ref T Ref(int index)
- {
- return ref Unsafe.AsRef((byte*)Data + index * Unsafe.SizeOf());
- }
+ public readonly ref T Ref(int index) => ref Unsafe.AsRef((byte*)this.Data + (index * Unsafe.SizeOf()));
- public IntPtr Address(int index)
- {
- return (IntPtr)((byte*)Data + index * Unsafe.SizeOf());
- }
+ public readonly nint Address(int index) => (nint)((byte*)this.Data + (index * Unsafe.SizeOf()));
}
///
/// A structure representing a dynamic array for unmanaged types.
///
/// The type of elements in the vector, must be unmanaged.
-public unsafe struct ImVector where T : unmanaged
+[StructLayout(LayoutKind.Sequential)]
+public unsafe struct ImVector : IEnumerable
+ where T : unmanaged
{
+ private int size;
+ private int capacity;
+ private T* data;
+
///
/// Initializes a new instance of the struct with the specified size, capacity, and data pointer.
///
@@ -45,11 +50,6 @@ public unsafe struct ImVector where T : unmanaged
this.data = data;
}
- private int size;
- private int capacity;
- private unsafe T* data;
-
-
///
/// Gets or sets the element at the specified index.
///
@@ -58,80 +58,72 @@ public unsafe struct ImVector where T : unmanaged
/// Thrown when the index is out of range.
public T this[int index]
{
- get
+ readonly get
{
- if (index < 0 || index >= size)
- {
+ if (index < 0 || index >= this.size)
throw new IndexOutOfRangeException();
- }
- return data[index];
+ return this.data[index];
}
set
{
- if (index < 0 || index >= size)
- {
+ if (index < 0 || index >= this.size)
throw new IndexOutOfRangeException();
- }
- data[index] = value;
+ this.data[index] = value;
}
}
///
/// Gets a pointer to the first element of the vector.
///
- public readonly T* Data => data;
+ public readonly T* Data => this.data;
///
/// Gets a pointer to the first element of the vector.
///
- public readonly T* Front => data;
+ public readonly T* Front => this.data;
///
/// Gets a pointer to the last element of the vector.
///
- public readonly T* Back => size > 0 ? data + size - 1 : null;
+ public readonly T* Back => this.size > 0 ? this.data + this.size - 1 : null;
///
/// Gets or sets the capacity of the vector.
///
public int Capacity
{
- readonly get => capacity;
+ readonly get => this.capacity;
set
{
- if (capacity == value)
- {
+ ArgumentOutOfRangeException.ThrowIfLessThan(value, this.size, nameof(Capacity));
+ if (this.capacity == value)
return;
- }
- if (data == null)
+ if (this.data == null)
{
- data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
+ this.data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
}
else
{
- int newSize = Math.Min(size, value);
- T* newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
- Buffer.MemoryCopy(data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
- ImGui.MemFree(data);
- data = newData;
- size = newSize;
+ var newSize = Math.Min(this.size, value);
+ var newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
+ Buffer.MemoryCopy(this.data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
+ ImGui.MemFree(this.data);
+ this.data = newData;
+ this.size = newSize;
}
- capacity = value;
+ this.capacity = value;
// Clear the rest of the data
- for (int i = size; i < capacity; i++)
- {
- data[i] = default;
- }
+ new Span(this.data + this.size, this.capacity - this.size).Clear();
}
}
///
/// Gets the number of elements in the vector.
///
- public readonly int Size => size;
+ public readonly int Size => this.size;
///
/// Grows the capacity of the vector to at least the specified value.
@@ -139,10 +131,8 @@ public unsafe struct ImVector where T : unmanaged
/// The new capacity.
public void Grow(int newCapacity)
{
- if (newCapacity > capacity)
- {
- Capacity = newCapacity * 2;
- }
+ var newCapacity2 = this.capacity > 0 ? this.capacity + (this.capacity / 2) : 8;
+ this.Capacity = newCapacity2 > newCapacity ? newCapacity2 : newCapacity;
}
///
@@ -151,10 +141,8 @@ public unsafe struct ImVector where T : unmanaged
/// The minimum capacity required.
public void EnsureCapacity(int size)
{
- if (size > capacity)
- {
+ if (size > this.capacity)
Grow(size);
- }
}
///
@@ -164,25 +152,46 @@ public unsafe struct ImVector where T : unmanaged
public void Resize(int newSize)
{
EnsureCapacity(newSize);
- size = newSize;
+ this.size = newSize;
}
///
/// Clears all elements from the vector.
///
- public void Clear()
+ public void Clear() => this.size = 0;
+
+ ///
+ /// Adds an element to the end of the vector.
+ ///
+ /// The value to add.
+ [OverloadResolutionPriority(1)]
+ public void PushBack(T value)
{
- size = 0;
+ this.EnsureCapacity(this.size + 1);
+ this.data[this.size++] = value;
}
///
/// Adds an element to the end of the vector.
///
/// The value to add.
- public void PushBack(T value)
+ [OverloadResolutionPriority(2)]
+ public void PushBack(in T value)
{
- EnsureCapacity(size + 1);
- data[size++] = value;
+ EnsureCapacity(this.size + 1);
+ this.data[this.size++] = value;
+ }
+
+ ///
+ /// Adds an element to the front of the vector.
+ ///
+ /// The value to add.
+ public void PushFront(in T value)
+ {
+ if (this.size == 0)
+ this.PushBack(value);
+ else
+ this.Insert(0, value);
}
///
@@ -190,48 +199,126 @@ public unsafe struct ImVector where T : unmanaged
///
public void PopBack()
{
- if (size > 0)
+ if (this.size > 0)
{
- size--;
+ this.size--;
}
}
+ public ref T Insert(int index, in T v) {
+ ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
+ this.EnsureCapacity(this.size + 1);
+ if (index < this.size)
+ {
+ Buffer.MemoryCopy(
+ this.data + index,
+ this.data + index + 1,
+ (this.size - index) * sizeof(T),
+ (this.size - index) * sizeof(T));
+ }
+
+ this.data[index] = v;
+ this.size++;
+ return ref this.data[index];
+ }
+
+ public Span InsertRange(int index, ReadOnlySpan v)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
+ this.EnsureCapacity(this.size + v.Length);
+ if (index < this.size)
+ {
+ Buffer.MemoryCopy(
+ this.data + index,
+ this.data + index + v.Length,
+ (this.size - index) * sizeof(T),
+ (this.size - index) * sizeof(T));
+ }
+
+ var dstSpan = new Span(this.data + index, v.Length);
+ v.CopyTo(new(this.data + index, v.Length));
+ this.size += v.Length;
+ return dstSpan;
+ }
+
///
/// Frees the memory allocated for the vector.
///
public void Free()
{
- if (data != null)
+ if (this.data != null)
{
- ImGui.MemFree(data);
- data = null;
- size = 0;
- capacity = 0;
+ ImGui.MemFree(this.data);
+ this.data = null;
+ this.size = 0;
+ this.capacity = 0;
}
}
- public ref T Ref(int index)
+ public readonly ref T Ref(int index)
{
- return ref Unsafe.AsRef((byte*)Data + index * Unsafe.SizeOf());
+ return ref Unsafe.AsRef((byte*)Data + (index * Unsafe.SizeOf()));
}
- public ref TCast Ref(int index)
+ public readonly ref TCast Ref(int index)
{
- return ref Unsafe.AsRef((byte*)Data + index * Unsafe.SizeOf());
+ return ref Unsafe.AsRef((byte*)Data + (index * Unsafe.SizeOf()));
}
- public void* Address(int index)
+ public readonly void* Address(int index)
{
- return (byte*)Data + index * Unsafe.SizeOf();
+ return (byte*)Data + (index * Unsafe.SizeOf());
}
- public void* Address(int index)
+ public readonly void* Address(int index)
{
- return (byte*)Data + index * Unsafe.SizeOf();
+ return (byte*)Data + (index * Unsafe.SizeOf());
}
- public ImVector* ToUntyped()
+ public readonly ImVector* ToUntyped()
{
- return (ImVector*)Unsafe.AsPointer(ref this);
+ return (ImVector*)Unsafe.AsPointer(ref Unsafe.AsRef(in this));
+ }
+
+ public readonly Span AsSpan() => new(this.data, this.size);
+
+ public readonly Enumerator GetEnumerator() => new(this.data, this.data + this.size);
+
+ readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ public struct Enumerator(T* begin, T* end) : IEnumerator, IEnumerable
+ {
+ private T* current = null;
+
+ public readonly ref T Current => ref *this.current;
+
+ readonly T IEnumerator.Current => this.Current;
+
+ readonly object IEnumerator.Current => this.Current;
+
+ public bool MoveNext()
+ {
+ var next = this.current == null ? begin : this.current + 1;
+ if (next == end)
+ return false;
+ this.current = next;
+ return true;
+ }
+
+ public void Reset() => this.current = null;
+
+ public readonly Enumerator GetEnumerator() => new(begin, end);
+
+ readonly void IDisposable.Dispose()
+ {
+ }
+
+ readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
}
From 544f8b28bfc115968ea53fbfb08207c6d7d010ea Mon Sep 17 00:00:00 2001
From: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Date: Sat, 16 Aug 2025 16:42:30 +0900
Subject: [PATCH 004/201] Support make clickthrough
---
...Win32InputHandler.StaticLookupFunctions.cs | 9 +-
.../InputHandler/Win32InputHandler.cs | 28 ++--
Dalamud/Interface/Windowing/Window.cs | 133 ++++++++----------
3 files changed, 79 insertions(+), 91 deletions(-)
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs
index 5710a5991..a7b70ce35 100644
--- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs
@@ -299,11 +299,12 @@ internal sealed partial class Win32InputHandler
private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle)
{
- style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW);
- exStyle =
- (int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW);
+ style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW;
+ exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW;
exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP;
- if (flags.HasFlag(ImGuiViewportFlags.TopMost))
+ if ((flags & ImGuiViewportFlags.TopMost) != 0)
exStyle |= WS.WS_EX_TOPMOST;
+ if ((flags & ImGuiViewportFlags.NoInputs) != 0)
+ exStyle |= WS.WS_EX_TRANSPARENT | WS.WS_EX_LAYERED;
}
}
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
index 62e254a1a..0b2e27b57 100644
--- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
@@ -8,6 +8,7 @@ using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Memory;
+using Dalamud.Utility;
using Serilog;
@@ -446,19 +447,19 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
ClientToScreen(this.hWnd, &pos);
SetCursorPos(pos.x, pos.y);
}
+ }
- // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
- if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
- {
- // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window)
- // (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.)
- // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor)
- // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.)
- var mousePos = mouseScreenPos;
- if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
- ClientToScreen(focusedWindow, &mousePos);
- io.AddMousePosEvent(mousePos.x, mousePos.y);
- }
+ // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
+ if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
+ {
+ // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window)
+ // (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.)
+ // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor)
+ // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.)
+ var mousePos = mouseScreenPos;
+ if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
+ ClientToScreen(focusedWindow, &mousePos);
+ io.AddMousePosEvent(mousePos.x, mousePos.y);
}
// (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering.
@@ -827,6 +828,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
null);
}
+ if (data->Hwnd == 0)
+ Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error");
+
data->HwndOwned = true;
viewport.PlatformRequestResize = false;
viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd;
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index a6b5e0801..e24f96ff8 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -1,9 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using System.Numerics;
-using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CheapLoc;
@@ -19,10 +16,13 @@ using Dalamud.Interface.Utility.Internal;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.Logging.Internal;
-using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI;
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
namespace Dalamud.Interface.Windowing;
///
@@ -31,11 +31,15 @@ namespace Dalamud.Interface.Windowing;
public abstract class Window
{
private const float FadeInOutTime = 0.072f;
+ private const string AdditionsPopupName = "WindowSystemContextActions";
private static readonly ModuleLog Log = new("WindowSystem");
private static bool wasEscPressedLastFrame = false;
+ private readonly TitleBarButton additionsButton;
+ private readonly List allButtons = [];
+
private bool internalLastIsOpen = false;
private bool internalIsOpen = false;
private bool internalIsPinned = false;
@@ -69,6 +73,20 @@ public abstract class Window
this.WindowName = name;
this.Flags = flags;
this.ForceMainWindow = forceMainWindow;
+
+ this.additionsButton = new()
+ {
+ Icon = FontAwesomeIcon.Bars,
+ IconOffset = new Vector2(2.5f, 1),
+ Click = _ =>
+ {
+ this.internalIsClickthrough = false;
+ this.presetDirty = false;
+ ImGui.OpenPopup(AdditionsPopupName);
+ },
+ Priority = int.MinValue,
+ AvailableClickthrough = true,
+ };
}
///
@@ -448,11 +466,12 @@ public abstract class Window
ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough;
}
- // Not supported yet on non-main viewports
- if (this.internalIsClickthrough && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
+ if (ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
{
- this.internalIsClickthrough = false;
- this.presetDirty = true;
+ if ((flags & ImGuiWindowFlags.NoInputs) == ImGuiWindowFlags.NoInputs)
+ ImGui.GetWindowViewport().Flags |= ImGuiViewportFlags.NoInputs;
+ else
+ ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
}
// Draw the actual window contents
@@ -466,7 +485,6 @@ public abstract class Window
}
}
- const string additionsPopupName = "WindowSystemContextActions";
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
@@ -477,7 +495,7 @@ public abstract class Window
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
- if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
+ if (ImGui.BeginPopup(AdditionsPopupName, ImGuiWindowFlags.NoMove))
{
if (this.internalIsClickthrough)
ImGui.BeginDisabled();
@@ -498,11 +516,6 @@ public abstract class Window
if (this.internalIsClickthrough)
ImGui.EndDisabled();
- var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
-
- if (!isAvailable)
- ImGui.BeginDisabled();
-
if (this.AllowClickthrough)
{
if (ImGui.Checkbox(
@@ -516,9 +529,6 @@ public abstract class Window
Loc.Localize("WindowSystemContextActionClickthroughHint", "Clickthrough windows will not receive mouse input, move or resize. They are completely inert."));
}
- if (!isAvailable)
- ImGui.EndDisabled();
-
var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
100f))
@@ -534,18 +544,11 @@ public abstract class Window
this.presetDirty = true;
}
- if (isAvailable)
- {
- ImGui.TextColored(ImGuiColors.DalamudGrey,
- Loc.Localize("WindowSystemContextActionClickthroughDisclaimer",
- "Open this menu again by clicking the three dashes to disable clickthrough."));
- }
- else
- {
- ImGui.TextColored(ImGuiColors.DalamudGrey,
- Loc.Localize("WindowSystemContextActionViewportDisclaimer",
- "These features are only available if this window is inside the game window."));
- }
+ ImGui.TextColored(
+ ImGuiColors.DalamudGrey,
+ Loc.Localize(
+ "WindowSystemContextActionClickthroughDisclaimer",
+ "Open this menu again by clicking the three dashes to disable clickthrough."));
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
printWindow = true;
@@ -556,34 +559,15 @@ public abstract class Window
ImGui.PopStyleVar();
}
- unsafe
+ if (flagsApplicableForTitleBarIcons)
{
- var window = ImGuiP.GetCurrentWindow();
-
- ImRect outRect;
- ImGuiP.TitleBarRect(&outRect, window);
-
- var additionsButton = new TitleBarButton
- {
- Icon = FontAwesomeIcon.Bars,
- IconOffset = new Vector2(2.5f, 1),
- Click = _ =>
- {
- this.internalIsClickthrough = false;
- this.presetDirty = false;
- ImGui.OpenPopup(additionsPopupName);
- },
- Priority = int.MinValue,
- AvailableClickthrough = true,
- };
-
- if (flagsApplicableForTitleBarIcons)
- {
- this.DrawTitleBarButtons(window, flags, outRect,
- showAdditions
- ? this.TitleBarButtons.Append(additionsButton)
- : this.TitleBarButtons);
- }
+ this.allButtons.Clear();
+ this.allButtons.EnsureCapacity(this.TitleBarButtons.Count + 1);
+ this.allButtons.AddRange(this.TitleBarButtons);
+ if (showAdditions)
+ this.allButtons.Add(this.additionsButton);
+ this.allButtons.Sort(static (a, b) => b.Priority - a.Priority);
+ this.DrawTitleBarButtons();
}
if (wasFocused)
@@ -740,8 +724,11 @@ public abstract class Window
}
}
- private unsafe void DrawTitleBarButtons(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect titleBarRect, IEnumerable buttons)
+ private unsafe void DrawTitleBarButtons()
{
+ var window = ImGuiP.GetCurrentWindow();
+ var flags = window.Flags;
+ var titleBarRect = window.TitleBarRect();
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
var style = ImGui.GetStyle();
@@ -776,26 +763,22 @@ public abstract class Window
var max = pos + new Vector2(fontSize, fontSize);
ImRect bb = new(pos, max);
var isClipped = !ImGuiP.ItemAdd(bb, id, null, 0);
- bool hovered, held;
- var pressed = false;
+ bool hovered, held, pressed;
if (this.internalIsClickthrough)
{
- hovered = false;
- held = false;
-
// ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves
- if (ImGui.IsMouseHoveringRect(pos, max))
- {
- hovered = true;
+ var pad = ImGui.GetStyle().TouchExtraPadding;
+ var rect = new ImRect(pos - pad, max + pad);
+ hovered = rect.Contains(ImGui.GetMousePos());
- // We can't use ImGui native functions here, because they don't work with clickthrough
- if ((global::Windows.Win32.PInvoke.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0)
- {
- held = true;
- pressed = true;
- }
- }
+ // Temporarily enable inputs
+ // This will be reset on next frame, and then enabled again if it is still being hovered
+ if (hovered && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
+ ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
+
+ // We can't use ImGui native functions here, because they don't work with clickthrough
+ pressed = held = hovered && (GetKeyState(VK.VK_LBUTTON) & 0x8000) != 0;
}
else
{
@@ -824,7 +807,7 @@ public abstract class Window
return pressed;
}
- foreach (var button in buttons.OrderBy(x => x.Priority))
+ foreach (var button in this.allButtons)
{
if (this.internalIsClickthrough && !button.AvailableClickthrough)
return;
@@ -932,7 +915,7 @@ public abstract class Window
///
/// Gets or sets an action that is called when the button is clicked.
///
- public Action Click { get; set; }
+ public Action? Click { get; set; }
///
/// Gets or sets the priority the button shall be shown in.
From b18b8b40e5b5505bc85c4895af2aa20e922e1ef8 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Mon, 25 Aug 2025 13:14:13 -0700
Subject: [PATCH 005/201] feat: Add IPC Context
- Demo of the ipc context idea
- Accessible via ipcPub.GetContext()
---
.../Windows/Data/Widgets/PluginIpcWidget.cs | 36 +++++++----
Dalamud/Plugin/DalamudPluginInterface.cs | 36 +++++------
Dalamud/Plugin/Ipc/ICallGateProvider.cs | 3 +
.../Plugin/Ipc/Internal/CallGateChannel.cs | 1 +
Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs | 56 +++++++++---------
.../Plugin/Ipc/Internal/CallGatePubSubBase.cs | 59 +++++++++++++++++--
Dalamud/Plugin/Ipc/IpcContext.cs | 15 +++++
7 files changed, 143 insertions(+), 63 deletions(-)
create mode 100644 Dalamud/Plugin/Ipc/IpcContext.cs
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs
index 6c581604e..446a5e7a9 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs
@@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Internal;
using Dalamud.Utility;
+
using Serilog;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@@ -48,12 +49,20 @@ internal class PluginIpcWidget : IDataWindowWidget
this.ipcPub.RegisterAction(msg =>
{
- Log.Information("Data action was called: {Msg}", msg);
+ Log.Information(
+ "Data action was called: {Msg}\n" +
+ " Context: {Context}",
+ msg,
+ this.ipcPub.GetContext());
});
this.ipcPub.RegisterFunc(msg =>
{
- Log.Information("Data func was called: {Msg}", msg);
+ Log.Information(
+ "Data func was called: {Msg}\n" +
+ " Context: {Context}",
+ msg,
+ this.ipcPub.GetContext());
return Guid.NewGuid().ToString();
});
}
@@ -61,14 +70,8 @@ internal class PluginIpcWidget : IDataWindowWidget
if (this.ipcSub == null)
{
this.ipcSub = new CallGatePubSub("dataDemo1");
- this.ipcSub.Subscribe(_ =>
- {
- Log.Information("PONG1");
- });
- this.ipcSub.Subscribe(_ =>
- {
- Log.Information("PONG2");
- });
+ this.ipcSub.Subscribe(_ => { Log.Information("PONG1"); });
+ this.ipcSub.Subscribe(_ => { Log.Information("PONG2"); });
this.ipcSub.Subscribe(_ => throw new Exception("PONG3"));
}
@@ -78,12 +81,21 @@ internal class PluginIpcWidget : IDataWindowWidget
this.ipcPubGo.RegisterAction(go =>
{
- Log.Information("Data action was called: {Name}", go?.Name);
+ Log.Information(
+ "Data action was called: {Name}" +
+ "\n Context: {Context}",
+ go?.Name,
+ this.ipcPubGo.GetContext());
});
this.ipcPubGo.RegisterFunc(go =>
{
- Log.Information("Data func was called: {Name}", go?.Name);
+ Log.Information(
+ "Data func was called: {Name}\n" +
+ " Context: {Context}",
+ go?.Name,
+ this.ipcPubGo.GetContext());
+
return "test";
});
}
diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs
index 541071b63..db9320079 100644
--- a/Dalamud/Plugin/DalamudPluginInterface.cs
+++ b/Dalamud/Plugin/DalamudPluginInterface.cs
@@ -293,39 +293,39 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
/// An IPC provider.
/// This is thrown when the requested types do not match the previously registered types are different.
public ICallGateProvider GetIpcProvider(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateProvider GetIpcProvider(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateProvider GetIpcProvider(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateProvider GetIpcProvider(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateProvider GetIpcProvider(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateProvider GetIpcProvider(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateProvider GetIpcProvider(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateProvider GetIpcProvider(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateProvider GetIpcProvider(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
/// Gets an IPC subscriber.
@@ -334,39 +334,39 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
/// The name of the IPC registration.
/// An IPC subscriber.
public ICallGateSubscriber GetIpcSubscriber(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateSubscriber GetIpcSubscriber(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateSubscriber GetIpcSubscriber(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateSubscriber GetIpcSubscriber(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateSubscriber GetIpcSubscriber(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateSubscriber GetIpcSubscriber(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateSubscriber GetIpcSubscriber(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateSubscriber GetIpcSubscriber(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
///
public ICallGateSubscriber GetIpcSubscriber(string name)
- => new CallGatePubSub(name);
+ => new CallGatePubSub(name, this.plugin);
#endregion
diff --git a/Dalamud/Plugin/Ipc/ICallGateProvider.cs b/Dalamud/Plugin/Ipc/ICallGateProvider.cs
index f4e5c76d7..387f0adf9 100644
--- a/Dalamud/Plugin/Ipc/ICallGateProvider.cs
+++ b/Dalamud/Plugin/Ipc/ICallGateProvider.cs
@@ -19,6 +19,9 @@ public interface ICallGateProvider
///
public void UnregisterFunc();
+
+ ///
+ public IpcContext? GetContext();
}
///
diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
index ea94103f7..698f0917e 100644
--- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
+++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
@@ -3,6 +3,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
+using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Ipc.Internal.Converters;
diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs
index cc54a563b..8725ef733 100644
--- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs
+++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs
@@ -1,3 +1,5 @@
+using Dalamud.Plugin.Internal.Types;
+
#pragma warning disable SA1402 // File may only contain a single type
namespace Dalamud.Plugin.Ipc.Internal;
@@ -5,9 +7,9 @@ namespace Dalamud.Plugin.Ipc.Internal;
///
internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber
{
- ///
- public CallGatePubSub(string name)
- : base(name)
+ ///
+ public CallGatePubSub(string name, LocalPlugin? owningPlugin = null)
+ : base(name, owningPlugin)
{
}
@@ -43,9 +45,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider
internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber
{
- ///
- public CallGatePubSub(string name)
- : base(name)
+ ///
+ public CallGatePubSub(string name, LocalPlugin? owningPlugin = null)
+ : base(name, owningPlugin)
{
}
@@ -81,9 +83,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider<
///
internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber
{
- ///
- public CallGatePubSub(string name)
- : base(name)
+ ///
+ public CallGatePubSub(string name, LocalPlugin? owningPlugin = null)
+ : base(name, owningPlugin)
{
}
@@ -119,9 +121,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvi
///
internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber
{
- ///
- public CallGatePubSub(string name)
- : base(name)
+ ///
+ public CallGatePubSub(string name, LocalPlugin? owningPlugin = null)
+ : base(name, owningPlugin)
{
}
@@ -157,9 +159,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateP
///
internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber
{
- ///
- public CallGatePubSub(string name)
- : base(name)
+ ///
+ public CallGatePubSub(string name, LocalPlugin? owningPlugin = null)
+ : base(name, owningPlugin)
{
}
@@ -195,9 +197,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallG
///
internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber
{
- ///
- public CallGatePubSub(string name)
- : base(name)
+ ///
+ public CallGatePubSub(string name, LocalPlugin? owningPlugin = null)
+ : base(name, owningPlugin)
{
}
@@ -233,9 +235,9 @@ internal class CallGatePubSub : CallGatePubSubBase, IC
///
internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber
{
- ///
- public CallGatePubSub(string name)
- : base(name)
+ ///
+ public CallGatePubSub(string name, LocalPlugin? owningPlugin = null)
+ : base(name, owningPlugin)
{
}
@@ -271,9 +273,9 @@ internal class CallGatePubSub : CallGatePubSubBase
///
internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber
{
- ///
- public CallGatePubSub(string name)
- : base(name)
+ ///
+ public CallGatePubSub(string name, LocalPlugin? owningPlugin = null)
+ : base(name, owningPlugin)
{
}
@@ -309,9 +311,9 @@ internal class CallGatePubSub : CallGatePubSub
///
internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber
{
- ///
- public CallGatePubSub(string name)
- : base(name)
+ ///
+ public CallGatePubSub(string name, LocalPlugin? owningPlugin = null)
+ : base(name, owningPlugin)
{
}
diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs
index 308457373..24cb5ca11 100644
--- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs
+++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs
@@ -1,4 +1,11 @@
+using System.Reactive.Disposables;
+using System.Threading;
+
+using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Ipc.Exceptions;
+using Dalamud.Utility;
+
+using Serilog;
namespace Dalamud.Plugin.Ipc.Internal;
@@ -7,13 +14,18 @@ namespace Dalamud.Plugin.Ipc.Internal;
///
internal abstract class CallGatePubSubBase
{
+ [ThreadStatic]
+ private static IpcContext? ipcExecutionContext;
+
///
/// Initializes a new instance of the class.
///
/// The name of the IPC registration.
- protected CallGatePubSubBase(string name)
+ /// The plugin that owns this IPC pubsub.
+ protected CallGatePubSubBase(string name, LocalPlugin? owningPlugin)
{
this.Channel = Service.Get().GetOrCreateChannel(name);
+ this.OwningPlugin = owningPlugin;
}
///
@@ -21,7 +33,7 @@ internal abstract class CallGatePubSubBase
/// s.
///
public bool HasAction => this.Channel.Action != null;
-
+
///
/// Gets a value indicating whether this IPC call gate has an associated Function. Only exposed to
/// s.
@@ -33,12 +45,17 @@ internal abstract class CallGatePubSubBase
/// s, and can be used to determine if messages should be sent through the gate.
///
public int SubscriptionCount => this.Channel.Subscriptions.Count;
-
+
///
/// Gets the underlying channel implementation.
///
protected CallGateChannel Channel { get; init; }
-
+
+ ///
+ /// Gets the plugin that owns this pubsub instance.
+ ///
+ protected LocalPlugin? OwningPlugin { get; init; }
+
///
/// Removes the associated Action from this call gate, effectively disabling RPC calls.
///
@@ -53,6 +70,16 @@ internal abstract class CallGatePubSubBase
public void UnregisterFunc()
=> this.Channel.Func = null;
+ ///
+ /// Gets the current context for this IPC call. This will only be present when called from within an IPC action
+ /// or function handler, and will be null otherwise.
+ ///
+ /// Returns a potential IPC context.
+ public IpcContext? GetContext()
+ {
+ return ipcExecutionContext;
+ }
+
///
/// Registers a for use by other plugins via RPC. This Delegate must satisfy the constraints
/// of an type as defined by the interface, meaning they may not return a value and must have
@@ -105,7 +132,12 @@ internal abstract class CallGatePubSubBase
///
///
private protected void InvokeAction(params object?[]? args)
- => this.Channel.InvokeAction(args);
+ {
+ using (this.BuildContext())
+ {
+ this.Channel.InvokeAction(args);
+ }
+ }
///
/// Executes the Function registered for this IPC call gate via . This method is intended
@@ -120,7 +152,12 @@ internal abstract class CallGatePubSubBase
///
///
private protected TRet InvokeFunc(params object?[]? args)
- => this.Channel.InvokeFunc(args);
+ {
+ using (this.BuildContext())
+ {
+ return this.Channel.InvokeFunc(args);
+ }
+ }
///
/// Send the given arguments to all subscribers (through ) of this IPC call gate. This method
@@ -132,4 +169,14 @@ internal abstract class CallGatePubSubBase
/// Delegate arguments.
private protected void SendMessage(params object?[]? args)
=> this.Channel.SendMessage(args);
+
+ private IDisposable BuildContext()
+ {
+ ipcExecutionContext = new IpcContext
+ {
+ SourcePlugin = this.OwningPlugin != null ? new ExposedPlugin(this.OwningPlugin) : null,
+ };
+
+ return Disposable.Create(() => { ipcExecutionContext = null; });
+ }
}
diff --git a/Dalamud/Plugin/Ipc/IpcContext.cs b/Dalamud/Plugin/Ipc/IpcContext.cs
new file mode 100644
index 000000000..25fde6a36
--- /dev/null
+++ b/Dalamud/Plugin/Ipc/IpcContext.cs
@@ -0,0 +1,15 @@
+namespace Dalamud.Plugin.Ipc;
+
+///
+/// The context associated for an IPC call. Reads from ThreadLocal.
+///
+public class IpcContext
+{
+ ///
+ /// Gets the plugin that initiated this IPC call.
+ ///
+ public IExposedPlugin? SourcePlugin { get; init; }
+
+ ///
+ public override string ToString() => $"";
+}
From 8cced4c1d7bece877a12cf69d0f1448a5b352c01 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Mon, 25 Aug 2025 13:31:05 -0700
Subject: [PATCH 006/201] fix: use channel threadlocal instead of a
ThreadStatic
---
Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs | 18 ++++++++++++++++++
.../Plugin/Ipc/Internal/CallGatePubSubBase.cs | 11 ++++-------
2 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
index 698f0917e..e177abab7 100644
--- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
+++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
+using System.Threading;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Ipc.Exceptions;
@@ -17,6 +18,8 @@ namespace Dalamud.Plugin.Ipc.Internal;
///
internal class CallGateChannel
{
+ private readonly ThreadLocal ipcExecutionContext = new();
+
///
/// The actual storage.
///
@@ -146,6 +149,21 @@ internal class CallGateChannel
return (TRet)result;
}
+ internal void SetInvocationContext(IpcContext ipcContext)
+ {
+ this.ipcExecutionContext.Value = ipcContext;
+ }
+
+ internal IpcContext? GetInvocationContext()
+ {
+ return this.ipcExecutionContext.IsValueCreated ? this.ipcExecutionContext.Value : null;
+ }
+
+ internal void ClearInvocationContext()
+ {
+ this.ipcExecutionContext.Value = null;
+ }
+
private void CheckAndConvertArgs(object?[]? args, MethodInfo methodInfo)
{
var paramTypes = methodInfo.GetParameters()
diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs
index 24cb5ca11..521824b7b 100644
--- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs
+++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs
@@ -14,9 +14,6 @@ namespace Dalamud.Plugin.Ipc.Internal;
///
internal abstract class CallGatePubSubBase
{
- [ThreadStatic]
- private static IpcContext? ipcExecutionContext;
-
///
/// Initializes a new instance of the class.
///
@@ -77,7 +74,7 @@ internal abstract class CallGatePubSubBase
/// Returns a potential IPC context.
public IpcContext? GetContext()
{
- return ipcExecutionContext;
+ return this.Channel.GetInvocationContext();
}
///
@@ -172,11 +169,11 @@ internal abstract class CallGatePubSubBase
private IDisposable BuildContext()
{
- ipcExecutionContext = new IpcContext
+ this.Channel.SetInvocationContext(new IpcContext
{
SourcePlugin = this.OwningPlugin != null ? new ExposedPlugin(this.OwningPlugin) : null,
- };
+ });
- return Disposable.Create(() => { ipcExecutionContext = null; });
+ return Disposable.Create(() => { this.Channel.ClearInvocationContext(); });
}
}
From 4e87b4b0076460195f3be5e2fe76630e41ebec2e Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 12 Nov 2025 20:15:12 +0100
Subject: [PATCH 007/201] Retarget to .NET 10
---
Directory.Build.props | 2 +-
global.json | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Directory.Build.props b/Directory.Build.props
index 4ed87c809..5f6da3d94 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -2,7 +2,7 @@
- net9.0-windows
+ net10.0-windows
x64
x64
13.0
diff --git a/global.json b/global.json
index ab1a4a2ec..93dd0dd1f 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
- "version": "9.0.0",
+ "version": "10.0.0",
"rollForward": "latestMinor",
"allowPrerelease": true
}
-}
+}
\ No newline at end of file
From 7d76d275559bd82ef0b53a9709a8d1530f007630 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 12 Nov 2025 20:31:28 +0100
Subject: [PATCH 008/201] Upgrade packages
---
Directory.Packages.props | 120 ++++++++++++++++++---------------------
build/build.csproj | 2 +-
2 files changed, 57 insertions(+), 65 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a1cef517e..91875e63e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,65 +1,57 @@
-
- true
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build/build.csproj b/build/build.csproj
index b4aaa959d..32907677f 100644
--- a/build/build.csproj
+++ b/build/build.csproj
@@ -12,6 +12,6 @@
-
+
From e0eff2fe74a91a4d234c3b916da7d61760cb9c9f Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 12 Nov 2025 21:02:07 +0100
Subject: [PATCH 009/201] Use standard apphost for Dalamud.Injector
---
.../Dalamud.Injector.Boot.vcxproj | 111 ------------------
.../Dalamud.Injector.Boot.vcxproj.filters | 67 -----------
Dalamud.Injector.Boot/main.cpp | 48 --------
Dalamud.Injector.Boot/pch.h | 1 -
Dalamud.Injector.Boot/resources.rc | 1 -
Dalamud.Injector/Dalamud.Injector.csproj | 3 +-
.../{EntryPoint.cs => Program.cs} | 26 +---
.../dalamud.ico | Bin
Dalamud.sln | 12 +-
9 files changed, 9 insertions(+), 260 deletions(-)
delete mode 100644 Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
delete mode 100644 Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters
delete mode 100644 Dalamud.Injector.Boot/main.cpp
delete mode 100644 Dalamud.Injector.Boot/pch.h
delete mode 100644 Dalamud.Injector.Boot/resources.rc
rename Dalamud.Injector/{EntryPoint.cs => Program.cs} (98%)
rename {Dalamud.Injector.Boot => Dalamud.Injector}/dalamud.ico (100%)
diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
deleted file mode 100644
index 7f8de3843..000000000
--- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
- {8874326B-E755-4D13-90B4-59AB263A3E6B}
- Dalamud_Injector_Boot
- Debug
- x64
-
-
-
- Debug
- x64
-
-
- Release
- x64
-
-
-
- 16.0
- Win32Proj
- 10.0
- Dalamud.Injector
-
-
-
- Application
- true
- v143
- false
- Unicode
- ..\bin\$(Configuration)\
- obj\$(Configuration)\
-
-
-
-
- Level3
- true
- true
- stdcpp23
- pch.h
- ProgramDatabase
- CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
-
-
- Console
- true
- false
- ..\lib\CoreCLR;%(AdditionalLibraryDirectories)
- $(OutDir)$(TargetName).Boot.pdb
-
-
-
-
- true
- false
- MultiThreadedDebugDLL
- _DEBUG;%(PreprocessorDefinitions)
-
-
- false
- false
-
-
-
-
- true
- true
- MultiThreadedDLL
- NDEBUG;%(PreprocessorDefinitions)
-
-
- true
- true
-
-
-
-
-
- nethost.dll
- PreserveNewest
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters
deleted file mode 100644
index 8f4372d89..000000000
--- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
- {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
- cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
-
-
- {93995380-89BD-4b04-88EB-625FBE52EBFB}
- h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
-
-
- {4faac519-3a73-4b2b-96e7-fb597f02c0be}
- ico;rc
-
-
-
-
- Resource Files
-
-
-
-
- Resource Files
-
-
-
-
- Source Files
-
-
- Source Files
-
-
- Source Files
-
-
- Source Files
-
-
- Source Files
-
-
-
-
- Header Files
-
-
- Header Files
-
-
- Header Files
-
-
- Header Files
-
-
- Header Files
-
-
- Header Files
-
-
- Header Files
-
-
-
\ No newline at end of file
diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp
deleted file mode 100644
index df4120009..000000000
--- a/Dalamud.Injector.Boot/main.cpp
+++ /dev/null
@@ -1,48 +0,0 @@
-#define WIN32_LEAN_AND_MEAN
-
-#include
-#include
-#include
-#include "..\Dalamud.Boot\logging.h"
-#include "..\lib\CoreCLR\CoreCLR.h"
-#include "..\lib\CoreCLR\boot.h"
-
-int wmain(int argc, wchar_t** argv)
-{
- // Take care: don't redirect stderr/out here, we need to write our pid to stdout for XL to read
- //logging::start_file_logging("dalamud.injector.boot.log", false);
- logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors");
- logging::I("Built at : " __DATE__ "@" __TIME__);
-
- wchar_t _module_path[MAX_PATH];
- GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
- std::filesystem::path fs_module_path(_module_path);
-
- std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str());
- std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str());
-
- // =========================================================================== //
-
- void* entrypoint_vfn;
- const auto result = InitializeClrAndGetEntryPoint(
- GetModuleHandleW(nullptr),
- false,
- runtimeconfig_path,
- module_path,
- L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
- L"Main",
- L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
- &entrypoint_vfn);
-
- if (FAILED(result))
- return result;
-
- typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
- custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn);
-
- logging::I("Running Dalamud Injector...");
- const auto ret = entrypoint_fn(argc, argv);
- logging::I("Done!");
-
- return ret;
-}
diff --git a/Dalamud.Injector.Boot/pch.h b/Dalamud.Injector.Boot/pch.h
deleted file mode 100644
index 6f70f09be..000000000
--- a/Dalamud.Injector.Boot/pch.h
+++ /dev/null
@@ -1 +0,0 @@
-#pragma once
diff --git a/Dalamud.Injector.Boot/resources.rc b/Dalamud.Injector.Boot/resources.rc
deleted file mode 100644
index 8369e82a1..000000000
--- a/Dalamud.Injector.Boot/resources.rc
+++ /dev/null
@@ -1 +0,0 @@
-MAINICON ICON "dalamud.ico"
diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj
index 4a55174a1..a0b4f6451 100644
--- a/Dalamud.Injector/Dalamud.Injector.csproj
+++ b/Dalamud.Injector/Dalamud.Injector.csproj
@@ -13,12 +13,13 @@
- Library
+ Exe
..\bin\$(Configuration)\
false
false
true
false
+ dalamud.ico
diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/Program.cs
similarity index 98%
rename from Dalamud.Injector/EntryPoint.cs
rename to Dalamud.Injector/Program.cs
index b876aa6ed..e224791e6 100644
--- a/Dalamud.Injector/EntryPoint.cs
+++ b/Dalamud.Injector/Program.cs
@@ -25,34 +25,20 @@ namespace Dalamud.Injector
///
/// Entrypoint to the program.
///
- public sealed class EntryPoint
+ public sealed class Program
{
- ///
- /// A delegate used during initialization of the CLR from Dalamud.Injector.Boot.
- ///
- /// Count of arguments.
- /// char** string arguments.
- /// Return value (HRESULT).
- public delegate int MainDelegate(int argc, IntPtr argvPtr);
-
///
/// Start the Dalamud injector.
///
- /// Count of arguments.
- /// byte** string arguments.
+ /// Command line arguments.
/// Return value (HRESULT).
- public static int Main(int argc, IntPtr argvPtr)
+ public static int Main(string[] argsArray)
{
try
{
- List args = new(argc);
-
- unsafe
- {
- var argv = (IntPtr*)argvPtr;
- for (var i = 0; i < argc; i++)
- args.Add(Marshal.PtrToStringUni(argv[i]));
- }
+ // API14 TODO: Refactor
+ var args = argsArray.ToList();
+ args.Insert(0, Assembly.GetExecutingAssembly().Location);
Init(args);
args.Remove("-v"); // Remove "verbose" flag
diff --git a/Dalamud.Injector.Boot/dalamud.ico b/Dalamud.Injector/dalamud.ico
similarity index 100%
rename from Dalamud.Injector.Boot/dalamud.ico
rename to Dalamud.Injector/dalamud.ico
diff --git a/Dalamud.sln b/Dalamud.sln
index c3af00f44..ee3c75b25 100644
--- a/Dalamud.sln
+++ b/Dalamud.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32319.34
MinimumVisualStudioVersion = 10.0.40219.1
@@ -27,8 +27,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
@@ -49,8 +47,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}"
@@ -103,10 +99,6 @@ Global
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
- {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
@@ -188,8 +180,6 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
- {8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
{4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
{E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
From a37a13e0ba0ab5a661f71add85c0b9740dabdf1c Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 12 Nov 2025 21:03:14 +0100
Subject: [PATCH 010/201] Use .NET 10 in CI
---
.github/workflows/main.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index be44afacc..299d71e95 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -23,7 +23,7 @@ jobs:
uses: microsoft/setup-msbuild@v1.0.2
- uses: actions/setup-dotnet@v3
with:
- dotnet-version: '9.0.200'
+ dotnet-version: '10.0.100'
- name: Define VERSION
run: |
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
From 7bc921f54328924e2b29ef481da08d481a60e4b7 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 12 Nov 2025 21:09:21 +0100
Subject: [PATCH 011/201] No analyzers on nuke build
---
build/build.csproj | 1 +
1 file changed, 1 insertion(+)
diff --git a/build/build.csproj b/build/build.csproj
index 32907677f..1e1416d92 100644
--- a/build/build.csproj
+++ b/build/build.csproj
@@ -13,5 +13,6 @@
+
From 928fbba4893ae2022a5b8b637c3fa875bc4afec4 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 12 Nov 2025 21:13:50 +0100
Subject: [PATCH 012/201] Remove Injector.Boot targets
---
build/DalamudBuild.cs | 19 +------------------
1 file changed, 1 insertion(+), 18 deletions(-)
diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs
index d374c79f8..ba2b09a4d 100644
--- a/build/DalamudBuild.cs
+++ b/build/DalamudBuild.cs
@@ -42,10 +42,7 @@ public class DalamudBuild : NukeBuild
AbsolutePath InjectorProjectDir => RootDirectory / "Dalamud.Injector";
AbsolutePath InjectorProjectFile => InjectorProjectDir / "Dalamud.Injector.csproj";
-
- AbsolutePath InjectorBootProjectDir => RootDirectory / "Dalamud.Injector.Boot";
- AbsolutePath InjectorBootProjectFile => InjectorBootProjectDir / "Dalamud.Injector.Boot.vcxproj";
-
+
AbsolutePath TestProjectDir => RootDirectory / "Dalamud.Test";
AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj";
@@ -172,14 +169,6 @@ public class DalamudBuild : NukeBuild
.EnableNoRestore());
});
- Target CompileInjectorBoot => _ => _
- .Executes(() =>
- {
- MSBuildTasks.MSBuild(s => s
- .SetTargetPath(InjectorBootProjectFile)
- .SetConfiguration(Configuration));
- });
-
Target SetCILogging => _ => _
.DependentFor(Compile)
.OnlyWhenStatic(() => IsCIBuild)
@@ -196,7 +185,6 @@ public class DalamudBuild : NukeBuild
.DependsOn(CompileDalamudBoot)
.DependsOn(CompileDalamudCrashHandler)
.DependsOn(CompileInjector)
- .DependsOn(CompileInjectorBoot)
;
Target CI => _ => _
@@ -250,11 +238,6 @@ public class DalamudBuild : NukeBuild
.SetProject(InjectorProjectFile)
.SetConfiguration(Configuration));
- MSBuildTasks.MSBuild(s => s
- .SetProjectFile(InjectorBootProjectFile)
- .SetConfiguration(Configuration)
- .SetTargets("Clean"));
-
FileSystemTasks.DeleteDirectory(ArtifactsDirectory);
Directory.CreateDirectory(ArtifactsDirectory);
});
From 6340afb6921bfd8915e22300bcf623c956ea0c1f Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 12 Nov 2025 21:39:38 +0100
Subject: [PATCH 013/201] Nuke schema, also remove analyzers from imgui testbed
---
.nuke/build.schema.json | 2 --
imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj | 1 +
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
index 8331affcc..03211ce8f 100644
--- a/.nuke/build.schema.json
+++ b/.nuke/build.schema.json
@@ -87,7 +87,6 @@
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
- "CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
@@ -115,7 +114,6 @@
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
- "CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
diff --git a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj
index d56faa31e..da31c9a8e 100644
--- a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj
+++ b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj
@@ -26,6 +26,7 @@
+
From 2b2f628096f25b00994f5fc5abec1acc2eb6327e Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 18:43:49 +0100
Subject: [PATCH 014/201] Convert ObjectTable enumerator to struct
---
.../Game/ClientState/Objects/ObjectTable.cs | 40 ++-----------------
1 file changed, 3 insertions(+), 37 deletions(-)
diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs
index 84c1b5693..598daf518 100644
--- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs
+++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs
@@ -12,8 +12,6 @@ using Dalamud.Utility;
using FFXIVClientStructs.Interop;
-using Microsoft.Extensions.ObjectPool;
-
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
@@ -34,8 +32,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
private readonly ClientState clientState;
private readonly CachedEntry[] cachedObjectTable;
- private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
-
[ServiceManager.ServiceConstructor]
private unsafe ObjectTable(ClientState clientState)
{
@@ -47,9 +43,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
this.cachedObjectTable = new CachedEntry[objectTableLength];
for (var i = 0; i < this.cachedObjectTable.Length; i++)
this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
-
- for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
- this.frameworkThreadEnumerators[i] = new(this, i);
}
///
@@ -239,30 +232,14 @@ internal sealed partial class ObjectTable
public IEnumerator GetEnumerator()
{
ThreadSafety.AssertMainThread();
-
- // If we're on the framework thread, see if there's an already allocated enumerator available for use.
- foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
- {
- if (x is not null)
- {
- var t = x;
- x = null;
- t.Reset();
- return t;
- }
- }
-
- // No reusable enumerator is available; allocate a new temporary one.
- return new Enumerator(this, -1);
+ return new Enumerator(this);
}
///
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
- private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator, IResettable
+ private struct Enumerator(ObjectTable owner) : IEnumerator
{
- private ObjectTable? owner = owner;
-
private int index = -1;
public IGameObject Current { get; private set; } = null!;
@@ -274,7 +251,7 @@ internal sealed partial class ObjectTable
if (this.index == objectTableLength)
return false;
- var cache = this.owner!.cachedObjectTable.AsSpan();
+ var cache = owner.cachedObjectTable.AsSpan();
for (this.index++; this.index < objectTableLength; this.index++)
{
if (cache[this.index].Update() is { } ao)
@@ -291,17 +268,6 @@ internal sealed partial class ObjectTable
public void Dispose()
{
- if (this.owner is not { } o)
- return;
-
- if (slotId != -1)
- o.frameworkThreadEnumerators[slotId] = this;
- }
-
- public bool TryReset()
- {
- this.Reset();
- return true;
}
}
}
From dd70c5b8eea1627566f0b355fc5ce7807007e803 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 18:36:45 +0100
Subject: [PATCH 015/201] Add struct enumerator to AetheryteList
---
.../ClientState/Aetherytes/AetheryteList.cs | 31 ++++++++++++++++---
1 file changed, 27 insertions(+), 4 deletions(-)
diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
index a3d44d423..f72339ed2 100644
--- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
+++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs
@@ -87,10 +87,7 @@ internal sealed partial class AetheryteList
///
public IEnumerator GetEnumerator()
{
- for (var i = 0; i < this.Length; i++)
- {
- yield return this[i];
- }
+ return new Enumerator(this);
}
///
@@ -98,4 +95,30 @@ internal sealed partial class AetheryteList
{
return this.GetEnumerator();
}
+
+ private struct Enumerator(AetheryteList aetheryteList) : IEnumerator
+ {
+ private int index = 0;
+
+ public IAetheryteEntry Current { get; private set; }
+
+ object IEnumerator.Current => this.Current;
+
+ public bool MoveNext()
+ {
+ if (this.index == aetheryteList.Length) return false;
+ this.Current = aetheryteList[this.index];
+ this.index++;
+ return true;
+ }
+
+ public void Reset()
+ {
+ this.index = 0;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
}
From 520e3ea028044395925e8d73d29a1a2fb4f5410f Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 18:37:15 +0100
Subject: [PATCH 016/201] Convert AetheryteEntry to readonly struct
---
.../ClientState/Aetherytes/AetheryteEntry.cs | 36 +++++++------------
1 file changed, 13 insertions(+), 23 deletions(-)
diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs
index 89dd8b8b1..e0a5df06d 100644
--- a/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs
+++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs
@@ -63,47 +63,37 @@ public interface IAetheryteEntry
}
///
-/// Class representing an aetheryte entry available to the game.
+/// This struct represents an aetheryte entry available to the game.
///
-internal sealed class AetheryteEntry : IAetheryteEntry
+/// Data read from the Aetheryte List.
+internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry
{
- private readonly TeleportInfo data;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Data read from the Aetheryte List.
- internal AetheryteEntry(TeleportInfo data)
- {
- this.data = data;
- }
+ ///
+ public uint AetheryteId => data.AetheryteId;
///
- public uint AetheryteId => this.data.AetheryteId;
+ public uint TerritoryId => data.TerritoryId;
///
- public uint TerritoryId => this.data.TerritoryId;
+ public byte SubIndex => data.SubIndex;
///
- public byte SubIndex => this.data.SubIndex;
+ public byte Ward => data.Ward;
///
- public byte Ward => this.data.Ward;
+ public byte Plot => data.Plot;
///
- public byte Plot => this.data.Plot;
+ public uint GilCost => data.GilCost;
///
- public uint GilCost => this.data.GilCost;
+ public bool IsFavourite => data.IsFavourite;
///
- public bool IsFavourite => this.data.IsFavourite;
+ public bool IsSharedHouse => data.IsSharedHouse;
///
- public bool IsSharedHouse => this.data.IsSharedHouse;
-
- ///
- public bool IsApartment => this.data.IsApartment;
+ public bool IsApartment => data.IsApartment;
///
public RowRef AetheryteData => LuminaUtils.CreateRef(this.AetheryteId);
From 8a9b47c7a472987c33240594fbea645727fb7dc3 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 18:37:35 +0100
Subject: [PATCH 017/201] Add struct enumerator to BuddyList
---
Dalamud/Game/ClientState/Buddy/BuddyList.cs | 31 ++++++++++++++++++---
1 file changed, 27 insertions(+), 4 deletions(-)
diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
index 84cfd24a3..71121e54e 100644
--- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs
+++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
@@ -130,12 +130,35 @@ internal sealed partial class BuddyList
///
public IEnumerator GetEnumerator()
{
- for (var i = 0; i < this.Length; i++)
- {
- yield return this[i];
- }
+ return new Enumerator(this);
}
///
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ private struct Enumerator(BuddyList buddyList) : IEnumerator
+ {
+ private int index = 0;
+
+ public IBuddyMember Current { get; private set; }
+
+ object IEnumerator.Current => this.Current;
+
+ public bool MoveNext()
+ {
+ if (this.index == buddyList.Length) return false;
+ this.Current = buddyList[this.index];
+ this.index++;
+ return true;
+ }
+
+ public void Reset()
+ {
+ this.index = 0;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
}
From 23e7c164d86ca1d59713f7f7f8955f2eed2b29d6 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 18:42:07 +0100
Subject: [PATCH 018/201] Convert BuddyMember to readonly struct
---
Dalamud/Game/ClientState/Buddy/BuddyList.cs | 33 +++++-----
Dalamud/Game/ClientState/Buddy/BuddyMember.cs | 60 ++++++++++++-------
2 files changed, 56 insertions(+), 37 deletions(-)
diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
index 71121e54e..78809f8ba 100644
--- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs
+++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
@@ -8,6 +8,9 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
+using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
+
namespace Dalamud.Game.ClientState.Buddy;
///
@@ -21,7 +24,7 @@ namespace Dalamud.Game.ClientState.Buddy;
#pragma warning restore SA1015
internal sealed partial class BuddyList : IServiceType, IBuddyList
{
- private const uint InvalidObjectID = 0xE0000000;
+ private const uint InvalidEntityId = 0xE0000000;
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service.Get();
@@ -69,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
}
- private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy;
+ private unsafe CSBuddy* BuddyListStruct => &UIState.Instance()->Buddy;
///
public IBuddyMember? this[int index]
@@ -82,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
///
- public unsafe IntPtr GetCompanionBuddyMemberAddress()
+ public unsafe nint GetCompanionBuddyMemberAddress()
{
- return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion;
+ return (nint)this.BuddyListStruct->CompanionInfo.Companion;
}
///
- public unsafe IntPtr GetPetBuddyMemberAddress()
+ public unsafe nint GetPetBuddyMemberAddress()
{
- return (IntPtr)this.BuddyListStruct->PetInfo.Pet;
+ return (nint)this.BuddyListStruct->PetInfo.Pet;
}
///
- public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
+ public unsafe nint GetBattleBuddyMemberAddress(int index)
{
if (index < 0 || index >= 3)
- return IntPtr.Zero;
+ return 0;
- return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
+ return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
}
///
- public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
+ public unsafe IBuddyMember? CreateBuddyMemberReference(nint address)
{
+ if (address == 0)
+ return null;
+
if (this.clientState.LocalContentId == 0)
return null;
- if (address == IntPtr.Zero)
- return null;
-
- var buddy = new BuddyMember(address);
- if (buddy.ObjectId == InvalidObjectID)
+ var buddy = new BuddyMember((CSBuddyMember*)address);
+ if (buddy.EntityId == InvalidEntityId)
return null;
return buddy;
diff --git a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs
index 393598d32..8018bafaf 100644
--- a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs
+++ b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs
@@ -1,20 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
+
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Excel;
+using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
+
namespace Dalamud.Game.ClientState.Buddy;
///
/// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
///
-public interface IBuddyMember
+public interface IBuddyMember : IEquatable
{
///
/// Gets the address of the buddy in memory.
///
- IntPtr Address { get; }
+ nint Address { get; }
///
/// Gets the object ID of this buddy.
@@ -67,42 +71,34 @@ public interface IBuddyMember
}
///
-/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
+/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
///
-internal unsafe class BuddyMember : IBuddyMember
+/// A pointer to the BuddyMember.
+internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
{
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service.Get();
- ///
- /// Initializes a new instance of the class.
- ///
- /// Buddy address.
- internal BuddyMember(IntPtr address)
- {
- this.Address = address;
- }
+ ///
+ public nint Address => (nint)ptr;
///
- public IntPtr Address { get; }
+ public uint ObjectId => this.EntityId;
///
- public uint ObjectId => this.Struct->EntityId;
+ public uint EntityId => ptr->EntityId;
///
- public uint EntityId => this.Struct->EntityId;
+ public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId);
///
- public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
+ public uint CurrentHP => ptr->CurrentHealth;
///
- public uint CurrentHP => this.Struct->CurrentHealth;
+ public uint MaxHP => ptr->MaxHealth;
///
- public uint MaxHP => this.Struct->MaxHealth;
-
- ///
- public uint DataID => this.Struct->DataId;
+ public uint DataID => ptr->DataId;
///
public RowRef MountData => LuminaUtils.CreateRef(this.DataID);
@@ -113,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember
///
public RowRef TrustData => LuminaUtils.CreateRef(this.DataID);
- private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
+ public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y);
+
+ public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y);
+
+ ///
+ public bool Equals(IBuddyMember? other)
+ {
+ return this.EntityId == other.EntityId;
+ }
+
+ ///
+ public override bool Equals([NotNullWhen(true)] object? obj)
+ {
+ return obj is BuddyMember fate && this.Equals(fate);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return this.EntityId.GetHashCode();
+ }
}
From d1bed3ebc5e5f4ec8a7104c2e718babb4b546425 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 18:42:31 +0100
Subject: [PATCH 019/201] Add struct enumerator to FateTable
---
Dalamud/Game/ClientState/Fates/FateTable.cs | 31 ++++++++++++++++++---
1 file changed, 27 insertions(+), 4 deletions(-)
diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs
index 1bf557ad5..942d1561f 100644
--- a/Dalamud/Game/ClientState/Fates/FateTable.cs
+++ b/Dalamud/Game/ClientState/Fates/FateTable.cs
@@ -110,12 +110,35 @@ internal sealed partial class FateTable
///
public IEnumerator GetEnumerator()
{
- for (var i = 0; i < this.Length; i++)
- {
- yield return this[i];
- }
+ return new Enumerator(this);
}
///
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ private struct Enumerator(FateTable fateTable) : IEnumerator
+ {
+ private int index = 0;
+
+ public IFate Current { get; private set; }
+
+ object IEnumerator.Current => this.Current;
+
+ public bool MoveNext()
+ {
+ if (this.index == fateTable.Length) return false;
+ this.Current = fateTable[this.index];
+ this.index++;
+ return true;
+ }
+
+ public void Reset()
+ {
+ this.index = 0;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
}
From a48eead85e9f9fb98fcaa841c34752b7dca700e2 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 18:43:06 +0100
Subject: [PATCH 020/201] Convert Fate to readonly struct
---
Dalamud/Game/ClientState/Fates/Fate.cs | 130 ++++++++------------
Dalamud/Game/ClientState/Fates/FateTable.cs | 22 ++--
2 files changed, 59 insertions(+), 93 deletions(-)
diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs
index 504b690c3..c40a8960e 100644
--- a/Dalamud/Game/ClientState/Fates/Fate.cs
+++ b/Dalamud/Game/ClientState/Fates/Fate.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Dalamud.Data;
@@ -6,10 +7,12 @@ using Dalamud.Memory;
using Lumina.Excel;
+using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
+
namespace Dalamud.Game.ClientState.Fates;
///
-/// Interface representing an fate entry that can be seen in the current area.
+/// Interface representing a fate entry that can be seen in the current area.
///
public interface IFate : IEquatable
{
@@ -111,133 +114,96 @@ public interface IFate : IEquatable
///
/// Gets the address of this Fate in memory.
///
- IntPtr Address { get; }
+ nint Address { get; }
}
///
-/// This class represents an FFXIV Fate.
+/// This struct represents a Fate.
///
-internal unsafe partial class Fate
+/// A pointer to the FateContext.
+internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The address of this fate in memory.
- internal Fate(IntPtr address)
- {
- this.Address = address;
- }
-
///
- public IntPtr Address { get; }
-
- private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address;
-
- public static bool operator ==(Fate fate1, Fate fate2)
- {
- if (fate1 is null || fate2 is null)
- return Equals(fate1, fate2);
-
- return fate1.Equals(fate2);
- }
-
- public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2);
-
- ///
- /// Gets a value indicating whether this Fate is still valid in memory.
- ///
- /// The fate to check.
- /// True or false.
- public static bool IsValid(Fate fate)
- {
- var clientState = Service.GetNullable();
-
- if (fate == null || clientState == null)
- return false;
-
- if (clientState.LocalContentId == 0)
- return false;
-
- return true;
- }
-
- ///
- /// Gets a value indicating whether this actor is still valid in memory.
- ///
- /// True or false.
- public bool IsValid() => IsValid(this);
+ public nint Address => (nint)ptr;
///
- bool IEquatable.Equals(IFate other) => this.FateId == other?.FateId;
-
- ///
- public override bool Equals(object obj) => ((IEquatable)this).Equals(obj as IFate);
-
- ///
- public override int GetHashCode() => this.FateId.GetHashCode();
-}
-
-///
-/// This class represents an FFXIV Fate.
-///
-internal unsafe partial class Fate : IFate
-{
- ///
- public ushort FateId => this.Struct->FateId;
+ public ushort FateId => ptr->FateId;
///
public RowRef GameData => LuminaUtils.CreateRef(this.FateId);
///
- public int StartTimeEpoch => this.Struct->StartTimeEpoch;
+ public int StartTimeEpoch => ptr->StartTimeEpoch;
///
- public short Duration => this.Struct->Duration;
+ public short Duration => ptr->Duration;
///
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
///
- public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name);
+ public SeString Name => MemoryHelper.ReadSeString(&ptr->Name);
///
- public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description);
+ public SeString Description => MemoryHelper.ReadSeString(&ptr->Description);
///
- public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective);
+ public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective);
///
- public FateState State => (FateState)this.Struct->State;
+ public FateState State => (FateState)ptr->State;
///
- public byte HandInCount => this.Struct->HandInCount;
+ public byte HandInCount => ptr->HandInCount;
///
- public byte Progress => this.Struct->Progress;
+ public byte Progress => ptr->Progress;
///
- public bool HasBonus => this.Struct->IsBonus;
+ public bool HasBonus => ptr->IsBonus;
///
- public uint IconId => this.Struct->IconId;
+ public uint IconId => ptr->IconId;
///
- public byte Level => this.Struct->Level;
+ public byte Level => ptr->Level;
///
- public byte MaxLevel => this.Struct->MaxLevel;
+ public byte MaxLevel => ptr->MaxLevel;
///
- public Vector3 Position => this.Struct->Location;
+ public Vector3 Position => ptr->Location;
///
- public float Radius => this.Struct->Radius;
+ public float Radius => ptr->Radius;
///
- public uint MapIconId => this.Struct->MapIconId;
+ public uint MapIconId => ptr->MapIconId;
///
/// Gets the territory this is located in.
///
- public RowRef TerritoryType => LuminaUtils.CreateRef(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId);
+ public RowRef TerritoryType => LuminaUtils.CreateRef(ptr->MapMarkers[0].MapMarkerData.TerritoryTypeId);
+
+ public static bool operator ==(Fate x, Fate y) => x.Equals(y);
+
+ public static bool operator !=(Fate x, Fate y) => !(x == y);
+
+ ///
+ public bool Equals(IFate? other)
+ {
+ return this.FateId == other.FateId;
+ }
+
+ ///
+ public override bool Equals([NotNullWhen(true)] object? obj)
+ {
+ return obj is Fate fate && this.Equals(fate);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return this.FateId.GetHashCode();
+ }
}
diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs
index 942d1561f..a6edf4a18 100644
--- a/Dalamud/Game/ClientState/Fates/FateTable.cs
+++ b/Dalamud/Game/ClientState/Fates/FateTable.cs
@@ -5,6 +5,7 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
+using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
namespace Dalamud.Game.ClientState.Fates;
@@ -25,7 +26,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable
}
///
- public unsafe IntPtr Address => (nint)CSFateManager.Instance();
+ public unsafe nint Address => (nint)CSFateManager.Instance();
///
public unsafe int Length
@@ -72,30 +73,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable
}
///
- public unsafe IntPtr GetFateAddress(int index)
+ public unsafe nint GetFateAddress(int index)
{
if (index >= this.Length)
- return IntPtr.Zero;
+ return 0;
var fateManager = CSFateManager.Instance();
if (fateManager == null)
- return IntPtr.Zero;
+ return 0;
- return (IntPtr)fateManager->Fates[index].Value;
+ return (nint)fateManager->Fates[index].Value;
}
///
- public IFate? CreateFateReference(IntPtr offset)
+ public unsafe IFate? CreateFateReference(IntPtr address)
{
- var clientState = Service.Get();
+ if (address == 0)
+ return null;
+ var clientState = Service.Get();
if (clientState.LocalContentId == 0)
return null;
- if (offset == IntPtr.Zero)
- return null;
-
- return new Fate(offset);
+ return new Fate((CSFateContext*)address);
}
}
From d1dc81318a8aa26fbf35da9de96fba3fb369edd5 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 18:47:55 +0100
Subject: [PATCH 021/201] Add struct enumerator to PartyList
---
Dalamud/Game/ClientState/Party/PartyList.cs | 46 ++++++++++++++++-----
1 file changed, 36 insertions(+), 10 deletions(-)
diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs
index a016a8211..bfd423a79 100644
--- a/Dalamud/Game/ClientState/Party/PartyList.cs
+++ b/Dalamud/Game/ClientState/Party/PartyList.cs
@@ -133,18 +133,44 @@ internal sealed partial class PartyList
///
public IEnumerator GetEnumerator()
{
- // Normally using Length results in a recursion crash, however we know the party size via ptr.
- for (var i = 0; i < this.Length; i++)
- {
- var member = this[i];
-
- if (member == null)
- break;
-
- yield return member;
- }
+ return new Enumerator(this);
}
///
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ private struct Enumerator(PartyList partyList) : IEnumerator
+ {
+ private int index = 0;
+
+ public IPartyMember Current { get; private set; }
+
+ object IEnumerator.Current => this.Current;
+
+ public bool MoveNext()
+ {
+ if (this.index == partyList.Length) return false;
+
+ for (; this.index < partyList.Length; this.index++)
+ {
+ var partyMember = partyList[this.index];
+ if (partyMember != null)
+ {
+ this.Current = partyMember;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void Reset()
+ {
+ this.index = 0;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
}
From 53b94caeb7470dc7f2ca639412188f38a7f19343 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 18:53:03 +0100
Subject: [PATCH 022/201] Convert PartyMember to readonly struct
---
Dalamud/Game/ClientState/Party/PartyList.cs | 31 ++++----
Dalamud/Game/ClientState/Party/PartyMember.cs | 79 +++++++++++--------
2 files changed, 62 insertions(+), 48 deletions(-)
diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs
index bfd423a79..ec22932ab 100644
--- a/Dalamud/Game/ClientState/Party/PartyList.cs
+++ b/Dalamud/Game/ClientState/Party/PartyList.cs
@@ -8,6 +8,7 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
+using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
namespace Dalamud.Game.ClientState.Party;
@@ -42,20 +43,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
///
- public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
+ public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance();
///
- public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
+ public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
///
- public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
+ public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
///
public long PartyId => this.GroupManagerStruct->MainGroup.PartyId;
- private static int PartyMemberSize { get; } = Marshal.SizeOf();
+ private static int PartyMemberSize { get; } = Marshal.SizeOf();
- private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
+ private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress;
///
public IPartyMember? this[int index]
@@ -80,45 +81,45 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
}
///
- public IntPtr GetPartyMemberAddress(int index)
+ public nint GetPartyMemberAddress(int index)
{
if (index < 0 || index >= GroupLength)
- return IntPtr.Zero;
+ return 0;
return this.GroupListAddress + (index * PartyMemberSize);
}
///
- public IPartyMember? CreatePartyMemberReference(IntPtr address)
+ public IPartyMember? CreatePartyMemberReference(nint address)
{
if (this.clientState.LocalContentId == 0)
return null;
- if (address == IntPtr.Zero)
+ if (address == 0)
return null;
- return new PartyMember(address);
+ return new PartyMember((CSPartyMember*)address);
}
///
- public IntPtr GetAllianceMemberAddress(int index)
+ public nint GetAllianceMemberAddress(int index)
{
if (index < 0 || index >= AllianceLength)
- return IntPtr.Zero;
+ return 0;
return this.AllianceListAddress + (index * PartyMemberSize);
}
///
- public IPartyMember? CreateAllianceMemberReference(IntPtr address)
+ public IPartyMember? CreateAllianceMemberReference(nint address)
{
if (this.clientState.LocalContentId == 0)
return null;
- if (address == IntPtr.Zero)
+ if (address == 0)
return null;
- return new PartyMember(address);
+ return new PartyMember((CSPartyMember*)address);
}
}
diff --git a/Dalamud/Game/ClientState/Party/PartyMember.cs b/Dalamud/Game/ClientState/Party/PartyMember.cs
index 4c738d866..c9980d9f2 100644
--- a/Dalamud/Game/ClientState/Party/PartyMember.cs
+++ b/Dalamud/Game/ClientState/Party/PartyMember.cs
@@ -1,26 +1,27 @@
+using System.Diagnostics.CodeAnalysis;
using System.Numerics;
-using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling;
-using Dalamud.Memory;
using Lumina.Excel;
+using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
+
namespace Dalamud.Game.ClientState.Party;
///
/// Interface representing a party member.
///
-public interface IPartyMember
+public interface IPartyMember : IEquatable
{
///
/// Gets the address of this party member in memory.
///
- IntPtr Address { get; }
+ nint Address { get; }
///
/// Gets a list of buffs or debuffs applied to this party member.
@@ -108,69 +109,81 @@ public interface IPartyMember
}
///
-/// This class represents a party member in the group manager.
+/// This struct represents a party member in the group manager.
///
-internal unsafe class PartyMember : IPartyMember
+/// A pointer to the PartyMember.
+internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// Address of the party member.
- internal PartyMember(IntPtr address)
- {
- this.Address = address;
- }
+ ///
+ public nint Address => (nint)ptr;
///
- public IntPtr Address { get; }
+ public StatusList Statuses => new(&ptr->StatusManager);
///
- public StatusList Statuses => new(&this.Struct->StatusManager);
+ public Vector3 Position => ptr->Position;
///
- public Vector3 Position => this.Struct->Position;
+ public long ContentId => (long)ptr->ContentId;
///
- public long ContentId => (long)this.Struct->ContentId;
+ public uint ObjectId => ptr->EntityId;
///
- public uint ObjectId => this.Struct->EntityId;
-
- ///
- public uint EntityId => this.Struct->EntityId;
+ public uint EntityId => ptr->EntityId;
///
public IGameObject? GameObject => Service.Get().SearchById(this.EntityId);
///
- public uint CurrentHP => this.Struct->CurrentHP;
+ public uint CurrentHP => ptr->CurrentHP;
///
- public uint MaxHP => this.Struct->MaxHP;
+ public uint MaxHP => ptr->MaxHP;
///
- public ushort CurrentMP => this.Struct->CurrentMP;
+ public ushort CurrentMP => ptr->CurrentMP;
///
- public ushort MaxMP => this.Struct->MaxMP;
+ public ushort MaxMP => ptr->MaxMP;
///
- public RowRef Territory => LuminaUtils.CreateRef(this.Struct->TerritoryType);
+ public RowRef Territory => LuminaUtils.CreateRef(ptr->TerritoryType);
///
- public RowRef World => LuminaUtils.CreateRef(this.Struct->HomeWorld);
+ public RowRef World => LuminaUtils.CreateRef(ptr->HomeWorld);
///
- public SeString Name => SeString.Parse(this.Struct->Name);
+ public SeString Name => SeString.Parse(ptr->Name);
///
- public byte Sex => this.Struct->Sex;
+ public byte Sex => ptr->Sex;
///
- public RowRef ClassJob => LuminaUtils.CreateRef(this.Struct->ClassJob);
+ public RowRef ClassJob => LuminaUtils.CreateRef(ptr->ClassJob);
///
- public byte Level => this.Struct->Level;
+ public byte Level => ptr->Level;
- private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;
+ public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y);
+
+ public static bool operator !=(PartyMember x, PartyMember y) => !(x == y);
+
+ ///
+ public bool Equals(IPartyMember? other)
+ {
+ return this.EntityId == other.EntityId;
+ }
+
+ ///
+ public override bool Equals([NotNullWhen(true)] object? obj)
+ {
+ return obj is PartyMember fate && this.Equals(fate);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return this.EntityId.GetHashCode();
+ }
}
From 7f2ed9adb6534934e6440b288fd6c753bddbe67c Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 19:01:04 +0100
Subject: [PATCH 023/201] Convert Status to readonly struct and add interface
---
Dalamud/Game/ClientState/Statuses/Status.cs | 86 +++++++++++++++------
1 file changed, 61 insertions(+), 25 deletions(-)
diff --git a/Dalamud/Game/ClientState/Statuses/Status.cs b/Dalamud/Game/ClientState/Statuses/Status.cs
index 2775f8f9b..160b15de5 100644
--- a/Dalamud/Game/ClientState/Statuses/Status.cs
+++ b/Dalamud/Game/ClientState/Statuses/Status.cs
@@ -1,61 +1,49 @@
+using System.Diagnostics.CodeAnalysis;
+
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Excel;
+using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
+
namespace Dalamud.Game.ClientState.Statuses;
///
-/// This class represents a status effect an actor is afflicted by.
+/// Interface representing a status.
///
-public unsafe class Status
+public interface IStatus : IEquatable
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// Status address.
- internal Status(IntPtr address)
- {
- this.Address = address;
- }
-
///
/// Gets the address of the status in memory.
///
- public IntPtr Address { get; }
+ nint Address { get; }
///
/// Gets the status ID of this status.
///
- public uint StatusId => this.Struct->StatusId;
+ uint StatusId { get; }
///
/// Gets the GameData associated with this status.
///
- public RowRef GameData => LuminaUtils.CreateRef(this.Struct->StatusId);
+ RowRef GameData { get; }
///
/// Gets the parameter value of the status.
///
- public ushort Param => this.Struct->Param;
-
- ///
- /// Gets the stack count of this status.
- /// Only valid if this is a non-food status.
- ///
- [Obsolete($"Replaced with {nameof(Param)}", true)]
- public byte StackCount => (byte)this.Struct->Param;
+ ushort Param { get; }
///
/// Gets the time remaining of this status.
///
- public float RemainingTime => this.Struct->RemainingTime;
+ float RemainingTime { get; }
///
/// Gets the source ID of this status.
///
- public uint SourceId => this.Struct->SourceObject.ObjectId;
+ uint SourceId { get; }
///
/// Gets the source actor associated with this status.
@@ -63,7 +51,55 @@ public unsafe class Status
///
/// This iterates the actor table, it should be used with care.
///
+ IGameObject? SourceObject { get; }
+}
+
+///
+/// This struct represents a status effect an actor is afflicted by.
+///
+/// A pointer to the Status.
+internal unsafe readonly struct Status(CSStatus* ptr) : IStatus
+{
+ ///
+ public nint Address => (nint)ptr;
+
+ ///
+ public uint StatusId => ptr->StatusId;
+
+ ///
+ public RowRef GameData => LuminaUtils.CreateRef(ptr->StatusId);
+
+ ///
+ public ushort Param => ptr->Param;
+
+ ///
+ public float RemainingTime => ptr->RemainingTime;
+
+ ///
+ public uint SourceId => ptr->SourceObject.ObjectId;
+
+ ///
public IGameObject? SourceObject => Service.Get().SearchById(this.SourceId);
- private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
+ public static bool operator ==(Status x, Status y) => x.Equals(y);
+
+ public static bool operator !=(Status x, Status y) => !(x == y);
+
+ ///
+ public bool Equals(IStatus? other)
+ {
+ return this.StatusId == other.StatusId && this.SourceId == other.SourceId && this.Param == other.Param && this.RemainingTime == other.RemainingTime;
+ }
+
+ ///
+ public override bool Equals([NotNullWhen(true)] object? obj)
+ {
+ return obj is Status fate && this.Equals(fate);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(this.StatusId, this.SourceId, this.Param, this.RemainingTime);
+ }
}
From 778c82fad2c369e59e442dab8b0a1e3eb7000373 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 19:02:50 +0100
Subject: [PATCH 024/201] Add struct enumerator to StatusList
---
.../Game/ClientState/Statuses/StatusList.cs | 77 +++++++++++++------
1 file changed, 53 insertions(+), 24 deletions(-)
diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs
index a38e45ea3..50d242d33 100644
--- a/Dalamud/Game/ClientState/Statuses/StatusList.cs
+++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
+
namespace Dalamud.Game.ClientState.Statuses;
///
@@ -14,7 +16,7 @@ public sealed unsafe partial class StatusList
/// Initializes a new instance of the class.
///
/// Address of the status list.
- internal StatusList(IntPtr address)
+ internal StatusList(nint address)
{
this.Address = address;
}
@@ -24,14 +26,14 @@ public sealed unsafe partial class StatusList
///
/// Pointer to the status list.
internal unsafe StatusList(void* pointer)
- : this((IntPtr)pointer)
+ : this((nint)pointer)
{
}
///
/// Gets the address of the status list in memory.
///
- public IntPtr Address { get; }
+ public nint Address { get; }
///
/// Gets the amount of status effect slots the actor has.
@@ -47,7 +49,7 @@ public sealed unsafe partial class StatusList
///
/// Status Index.
/// The status at the specified index.
- public Status? this[int index]
+ public IStatus? this[int index]
{
get
{
@@ -64,7 +66,7 @@ public sealed unsafe partial class StatusList
///
/// The address of the status list in memory.
/// The status object containing the requested data.
- public static StatusList? CreateStatusListReference(IntPtr address)
+ public static StatusList? CreateStatusListReference(nint address)
{
// The use case for CreateStatusListReference and CreateStatusReference to be static is so
// fake status lists can be generated. Since they aren't exposed as services, it's either
@@ -74,7 +76,7 @@ public sealed unsafe partial class StatusList
if (clientState.LocalContentId == 0)
return null;
- if (address == IntPtr.Zero)
+ if (address == 0)
return null;
return new StatusList(address);
@@ -85,17 +87,17 @@ public sealed unsafe partial class StatusList
///
/// The address of the status effect in memory.
/// The status object containing the requested data.
- public static Status? CreateStatusReference(IntPtr address)
+ public static IStatus? CreateStatusReference(nint address)
{
var clientState = Service.Get();
if (clientState.LocalContentId == 0)
return null;
- if (address == IntPtr.Zero)
+ if (address == 0)
return null;
- return new Status(address);
+ return new Status((CSStatus*)address);
}
///
@@ -103,22 +105,22 @@ public sealed unsafe partial class StatusList
///
/// The index of the status.
/// The memory address of the status.
- public IntPtr GetStatusAddress(int index)
+ public nint GetStatusAddress(int index)
{
if (index < 0 || index >= this.Length)
- return IntPtr.Zero;
+ return 0;
- return (IntPtr)Unsafe.AsPointer(ref this.Struct->Status[index]);
+ return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]);
}
}
///
/// This collection represents the status effects an actor is afflicted by.
///
-public sealed partial class StatusList : IReadOnlyCollection, ICollection
+public sealed partial class StatusList : IReadOnlyCollection, ICollection
{
///
- int IReadOnlyCollection.Count => this.Length;
+ int IReadOnlyCollection.Count => this.Length;
///
int ICollection.Count => this.Length;
@@ -130,17 +132,9 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio
object ICollection.SyncRoot => this;
///
- public IEnumerator GetEnumerator()
+ public IEnumerator GetEnumerator()
{
- for (var i = 0; i < this.Length; i++)
- {
- var status = this[i];
-
- if (status == null || status.StatusId == 0)
- continue;
-
- yield return status;
- }
+ return new Enumerator(this);
}
///
@@ -155,4 +149,39 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio
index++;
}
}
+
+ private struct Enumerator(StatusList statusList) : IEnumerator
+ {
+ private int index = 0;
+
+ public IStatus Current { get; private set; }
+
+ object IEnumerator.Current => this.Current;
+
+ public bool MoveNext()
+ {
+ if (this.index == statusList.Length) return false;
+
+ for (; this.index < statusList.Length; this.index++)
+ {
+ var status = statusList[this.index];
+ if (status != null && status.StatusId != 0)
+ {
+ this.Current = status;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void Reset()
+ {
+ this.index = 0;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
}
From 9d0879148c740942523da5310c5b8039cb3be707 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 13 Nov 2025 19:05:25 +0100
Subject: [PATCH 025/201] Remove unused StatusEffect struct
---
.../Game/ClientState/Structs/StatusEffect.cs | 35 -------------------
1 file changed, 35 deletions(-)
delete mode 100644 Dalamud/Game/ClientState/Structs/StatusEffect.cs
diff --git a/Dalamud/Game/ClientState/Structs/StatusEffect.cs b/Dalamud/Game/ClientState/Structs/StatusEffect.cs
deleted file mode 100644
index 2a60a7d3b..000000000
--- a/Dalamud/Game/ClientState/Structs/StatusEffect.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Runtime.InteropServices;
-
-namespace Dalamud.Game.ClientState.Structs;
-
-///
-/// Native memory representation of a FFXIV status effect.
-///
-[StructLayout(LayoutKind.Sequential)]
-public struct StatusEffect
-{
- ///
- /// The effect ID.
- ///
- public short EffectId;
-
- ///
- /// How many stacks are present.
- ///
- public byte StackCount;
-
- ///
- /// Additional parameters.
- ///
- public byte Param;
-
- ///
- /// The duration remaining.
- ///
- public float Duration;
-
- ///
- /// The ID of the actor that caused this effect.
- ///
- public int OwnerId;
-}
From 78ed4a2b01f2fb4f4ff86ea5eab2c7eca7e6b015 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Sun, 16 Nov 2025 15:55:35 -0800
Subject: [PATCH 026/201] feat: Dalamud RPC service
A draft for a simple RPC service for Dalamud. Enables use of Dalamud URIs, to be added later.
---
Dalamud.Test/Pipes/DalamudUriTests.cs | 107 +++++++++++
Dalamud/Dalamud.csproj | 1 +
.../Networking/Pipes/Api/PluginLinkHandler.cs | 53 ++++++
Dalamud/Networking/Pipes/DalamudUri.cs | 102 +++++++++++
.../Pipes/Internal/ClientHelloService.cs | 94 ++++++++++
.../Pipes/Internal/LinkHandlerService.cs | 129 ++++++++++++++
Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs | 167 ++++++++++++++++++
Dalamud/Networking/Pipes/Rpc/RpcConnection.cs | 92 ++++++++++
.../Networking/Pipes/Rpc/RpcHostService.cs | 49 +++++
.../Pipes/Rpc/RpcServiceRegistry.cs | 85 +++++++++
Dalamud/Plugin/Services/IPluginLinkHandler.cs | 20 +++
Directory.Packages.props | 13 +-
12 files changed, 911 insertions(+), 1 deletion(-)
create mode 100644 Dalamud.Test/Pipes/DalamudUriTests.cs
create mode 100644 Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
create mode 100644 Dalamud/Networking/Pipes/DalamudUri.cs
create mode 100644 Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
create mode 100644 Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
create mode 100644 Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcConnection.cs
create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcHostService.cs
create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs
create mode 100644 Dalamud/Plugin/Services/IPluginLinkHandler.cs
diff --git a/Dalamud.Test/Pipes/DalamudUriTests.cs b/Dalamud.Test/Pipes/DalamudUriTests.cs
new file mode 100644
index 000000000..4977f3814
--- /dev/null
+++ b/Dalamud.Test/Pipes/DalamudUriTests.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Linq;
+
+using Dalamud.Networking.Pipes;
+using Xunit;
+
+namespace Dalamud.Test.Pipes
+{
+ public class DalamudUriTests
+ {
+ [Theory]
+ [InlineData("https://www.google.com/", false)]
+ [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)]
+ public void ValidatesScheme(string uri, bool valid)
+ {
+ Action act = () => { _ = DalamudUri.FromUri(uri); };
+
+ var ex = Record.Exception(act);
+ if (valid)
+ {
+ Assert.Null(ex);
+ }
+ else
+ {
+ Assert.NotNull(ex);
+ Assert.IsType(ex);
+ }
+ }
+
+ [Theory]
+ [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")]
+ [InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")]
+ [InlineData("dalamud://Test", "test")]
+ public void ExtractsNamespace(string uri, string expectedNamespace)
+ {
+ var dalamudUri = DalamudUri.FromUri(uri);
+ Assert.Equal(expectedNamespace, dalamudUri.Namespace);
+ }
+
+ [Theory]
+ [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")]
+ [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")]
+ [InlineData("dalamud://foo/bar/baz", "/bar/baz")]
+ [InlineData("dalamud://foo/bar", "/bar")]
+ [InlineData("dalamud://foo/bar/", "/bar/")]
+ [InlineData("dalamud://foo/", "/")]
+ public void ExtractsPath(string uri, string expectedPath)
+ {
+ var dalamudUri = DalamudUri.FromUri(uri);
+ Assert.Equal(expectedPath, dalamudUri.Path);
+ }
+
+ [Theory]
+ [InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")]
+ [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")]
+ [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")]
+ [InlineData("dalamud://foo/bar/baz", "/bar/baz")]
+ [InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")]
+ [InlineData("dalamud://foo/bar", "/bar")]
+ [InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")]
+ [InlineData("dalamud://foo/bar/", "/bar/")]
+ [InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")]
+ [InlineData("dalamud://foo/?cow=moo", "/?cow=moo")]
+ [InlineData("dalamud://foo/", "/")]
+ public void ExtractsData(string uri, string expectedData)
+ {
+ var dalamudUri = DalamudUri.FromUri(uri);
+
+ Assert.Equal(expectedData, dalamudUri.Data);
+ }
+
+ [Theory]
+ [InlineData("dalamud://foo/bar", 0)]
+ [InlineData("dalamud://foo/bar?cow=moo", 1)]
+ [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)]
+ [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)]
+ public void ExtractsQueryParams(string uri, int queryCount)
+ {
+ var dalamudUri = DalamudUri.FromUri(uri);
+ Assert.Equal(queryCount, dalamudUri.QueryParams.Count);
+ }
+
+ [Theory]
+ [InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)]
+ [InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)]
+ [InlineData("dalamud://foo/bar/baz/qux/meh", 5)]
+ [InlineData("dalamud://foo/bar/baz/qux", 4)]
+ [InlineData("dalamud://foo/bar/baz", 3)]
+ [InlineData("dalamud://foo/bar/", 2)]
+ [InlineData("dalamud://foo/bar", 2)]
+ public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false)
+ {
+ var dalamudUri = DalamudUri.FromUri(uri);
+ var segments = dalamudUri.Segments;
+
+ // First segment must always be `/`
+ Assert.Equal("/", segments[0]);
+
+ Assert.Equal(segmentCount, segments.Length);
+
+ if (finalSegmentEndsWithSlash)
+ {
+ Assert.EndsWith("/", segments.Last());
+ }
+ }
+ }
+}
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 1e5f9f586..849a5ce7f 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -81,6 +81,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
new file mode 100644
index 000000000..2c99901b4
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
@@ -0,0 +1,53 @@
+using System.Linq;
+
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Networking.Pipes.Internal;
+using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Services;
+
+namespace Dalamud.Networking.Pipes.Api;
+
+///
+[PluginInterface]
+[ServiceManager.ScopedService]
+[ResolveVia]
+public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler
+{
+ private readonly LinkHandlerService linkHandler;
+ private readonly LocalPlugin localPlugin;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The plugin to bind this service to.
+ /// The central link handler.
+ internal PluginLinkHandler(LocalPlugin localPlugin, LinkHandlerService linkHandler)
+ {
+ this.linkHandler = linkHandler;
+ this.localPlugin = localPlugin;
+
+ this.linkHandler.Register("plugin", this.HandleUri);
+ }
+
+ ///
+ public event IPluginLinkHandler.PluginUriReceived? OnUriReceived;
+
+ ///
+ public void DisposeService()
+ {
+ this.OnUriReceived = null;
+ this.linkHandler.Unregister("plugin", this.HandleUri);
+ }
+
+ private void HandleUri(DalamudUri uri)
+ {
+ var target = uri.Path.Split("/").FirstOrDefault();
+ if (target == null || !string.Equals(target, this.localPlugin.InternalName, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ this.OnUriReceived?.Invoke(uri);
+ }
+}
diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Pipes/DalamudUri.cs
new file mode 100644
index 000000000..03ad15af1
--- /dev/null
+++ b/Dalamud/Networking/Pipes/DalamudUri.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Web;
+
+namespace Dalamud.Networking.Pipes;
+
+///
+/// A Dalamud Uri, in the format:
+/// dalamud://{NAMESPACE}/{ARBITRARY}
+///
+public record DalamudUri
+{
+ private readonly Uri rawUri;
+
+ private DalamudUri(Uri uri)
+ {
+ if (uri.Scheme != "dalamud")
+ {
+ throw new ArgumentOutOfRangeException(nameof(uri), "URI must be of scheme dalamud.");
+ }
+
+ this.rawUri = uri;
+ }
+
+ ///
+ /// Gets the namespace that this URI should be routed to. Generally a high level component like "PluginInstaller".
+ ///
+ public string Namespace => this.rawUri.Authority;
+
+ ///
+ /// Gets the raw (untargeted) path and query params for this URI.
+ ///
+ public string Data =>
+ this.rawUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped);
+
+ ///
+ /// Gets the raw (untargeted) path for this URI.
+ ///
+ public string Path => this.rawUri.AbsolutePath;
+
+ ///
+ /// Gets a list of segments based on the provided Data element.
+ ///
+ public string[] Segments => this.GetDataSegments();
+
+ ///
+ /// Gets the raw query parameters for this URI, if any.
+ ///
+ public string Query => this.rawUri.Query;
+
+ ///
+ /// Gets the query params (as a parsed NameValueCollection) in this URI.
+ ///
+ public NameValueCollection QueryParams => HttpUtility.ParseQueryString(this.Query);
+
+ ///
+ /// Gets the fragment (if one is specified) in this URI.
+ ///
+ public string Fragment => this.rawUri.Fragment;
+
+ ///
+ public override string ToString() => this.rawUri.ToString();
+
+ private string[] GetDataSegments()
+ {
+ // reimplementation of the System.URI#Segments, under MIT license.
+ var path = this.Path;
+
+ var segments = new List();
+ var current = 0;
+ while (current < path.Length)
+ {
+ var next = path.IndexOf('/', current);
+ if (next == -1)
+ {
+ next = path.Length - 1;
+ }
+
+ segments.Add(path.Substring(current, (next - current) + 1));
+ current = next + 1;
+ }
+
+ return segments.ToArray();
+ }
+
+ ///
+ /// Build a DalamudURI from a given URI.
+ ///
+ /// The URI to convert to a Dalamud URI.
+ /// Returns a DalamudUri.
+ public static DalamudUri FromUri(Uri uri)
+ {
+ return new DalamudUri(uri);
+ }
+
+ ///
+ /// Build a DalamudURI from a URI in string format.
+ ///
+ /// The URI to convert to a Dalamud URI.
+ /// Returns a DalamudUri.
+ public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri));
+}
diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
new file mode 100644
index 000000000..cc06560bd
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
@@ -0,0 +1,94 @@
+using System.Threading.Tasks;
+
+using Dalamud.Game;
+using Dalamud.Game.ClientState;
+using Dalamud.Networking.Pipes.Rpc;
+using Dalamud.Utility;
+
+namespace Dalamud.Networking.Pipes.Internal;
+
+///
+/// A minimal service to respond with information about this client.
+///
+[ServiceManager.EarlyLoadedService]
+internal sealed class ClientHelloService : IInternalDisposableService
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Injected host service.
+ [ServiceManager.ServiceConstructor]
+ public ClientHelloService(RpcHostService rpcHostService)
+ {
+ rpcHostService.AddMethod("hello", this.HandleHello);
+ }
+
+ ///
+ /// Handle a hello request.
+ ///
+ /// .
+ /// Respond with information.
+ public async Task HandleHello(ClientHelloRequest request)
+ {
+ var framework = await Service.GetAsync();
+ var dalamud = await Service.GetAsync();
+ var clientState = await Service.GetAsync();
+
+ var response = await framework.RunOnFrameworkThread(() => new ClientHelloResponse
+ {
+ ApiVersion = "1.0",
+ DalamudVersion = Util.GetScmVersion(),
+ GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown",
+ PlayerName = clientState.IsLoggedIn ? clientState.LocalPlayer?.Name.ToString() ?? "Unknown" : null,
+ });
+
+ return response;
+ }
+
+ ///
+ public void DisposeService()
+ {
+ }
+}
+
+///
+/// A request from a client to say hello.
+///
+internal record ClientHelloRequest
+{
+ ///
+ /// Gets the API version this client is expecting.
+ ///
+ public string ApiVersion { get; init; } = string.Empty;
+
+ ///
+ /// Gets the user agent of the client.
+ ///
+ public string UserAgent { get; init; } = string.Empty;
+}
+
+///
+/// A response from Dalamud to a hello request.
+///
+internal record ClientHelloResponse
+{
+ ///
+ /// Gets the API version this server has offered.
+ ///
+ public string? ApiVersion { get; init; }
+
+ ///
+ /// Gets the current Dalamud version.
+ ///
+ public string? DalamudVersion { get; init; }
+
+ ///
+ /// Gets the current game version.
+ ///
+ public string? GameVersion { get; init; }
+
+ ///
+ /// Gets or sets the player name, or null if the player isn't logged in.
+ ///
+ public string? PlayerName { get; set; }
+}
diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
new file mode 100644
index 000000000..79bb1e017
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
@@ -0,0 +1,129 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+using Dalamud.Logging.Internal;
+using Dalamud.Networking.Pipes.Rpc;
+
+namespace Dalamud.Networking.Pipes.Internal;
+
+///
+/// A service responsible for handling Dalamud URIs and dispatching them accordingly.
+///
+[ServiceManager.EarlyLoadedService]
+internal class LinkHandlerService : IInternalDisposableService
+{
+ private readonly ModuleLog log = new("LinkHandler");
+
+ // key: namespace (e.g. "plugin" or "PluginInstaller") -> list of handlers
+ private readonly ConcurrentDictionary>> handlers
+ = new(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The injected RPC host service.
+ [ServiceManager.ServiceConstructor]
+ public LinkHandlerService(RpcHostService rpcHostService)
+ {
+ rpcHostService.AddMethod("handleLink", this.HandleLinkCall);
+ }
+
+ ///
+ public void DisposeService()
+ {
+ }
+
+ ///
+ /// Register a handler for a namespace. All URIs with this namespace will be dispatched to the handler.
+ ///
+ /// The namespace to use for this subscription.
+ /// The command handler.
+ public void Register(string ns, Action handler)
+ {
+ if (string.IsNullOrWhiteSpace(ns))
+ throw new ArgumentNullException(nameof(ns));
+
+ var list = this.handlers.GetOrAdd(ns, _ => []);
+ lock (list)
+ {
+ list.Add(handler);
+ }
+
+ this.log.Verbose("Registered handler for {Namespace}", ns);
+ }
+
+ ///
+ /// Unregister a handler.
+ ///
+ /// The namespace to use for this subscription.
+ /// The command handler.
+ public void Unregister(string ns, Action handler)
+ {
+ if (string.IsNullOrWhiteSpace(ns))
+ return;
+
+ if (!this.handlers.TryGetValue(ns, out var list))
+ return;
+
+ lock (list)
+ {
+ list.RemoveAll(x => x == handler);
+ }
+
+ if (list.Count == 0)
+ this.handlers.TryRemove(ns, out _);
+
+ this.log.Verbose("Unregistered handler for {Namespace}", ns);
+ }
+
+ ///
+ /// Dispatch a URI to matching handlers.
+ ///
+ /// The URI to parse and dispatch.
+ public void Dispatch(DalamudUri uri)
+ {
+ this.log.Information("Received URI: {Uri}", uri.ToString());
+
+ var ns = uri.Namespace;
+ if (!this.handlers.TryGetValue(ns, out var list))
+ return;
+
+ Action[] snapshot;
+ lock (list)
+ {
+ snapshot = list.ToArray();
+ }
+
+ foreach (var h in snapshot)
+ {
+ try
+ {
+ h(uri);
+ }
+ catch (Exception e)
+ {
+ this.log.Warning(e, "Link handler threw for {UriPath}", uri.Path);
+ }
+ }
+ }
+
+ ///
+ /// The RPC-invokable link handler.
+ ///
+ /// A plain-text URI to parse.
+ public void HandleLinkCall(string uri)
+ {
+ if (string.IsNullOrWhiteSpace(uri))
+ return;
+
+ try
+ {
+ var du = DalamudUri.FromUri(uri);
+ this.Dispatch(du);
+ }
+ catch (Exception)
+ {
+ // swallow parse errors; clients shouldn't crash the host
+ }
+ }
+}
diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
new file mode 100644
index 000000000..07dc9d96a
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
@@ -0,0 +1,167 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO.Pipes;
+using System.Security.AccessControl;
+using System.Security.Principal;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Logging.Internal;
+using Dalamud.Utility;
+
+namespace Dalamud.Networking.Pipes.Rpc;
+
+///
+/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc.
+///
+internal class PipeRpcHost : IDisposable
+{
+ private readonly ModuleLog log = new("RPC/Host");
+
+ private readonly RpcServiceRegistry registry = new();
+ private readonly CancellationTokenSource cts = new();
+ private readonly ConcurrentDictionary sessions = new();
+ private Task? acceptLoopTask;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The pipe name to create.
+ public PipeRpcHost(string? pipeName = null)
+ {
+ // Default pipe name based on current process ID for uniqueness per Dalamud instance.
+ this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}";
+ }
+
+ ///
+ /// Gets the name of the named pipe this RPC host is using.
+ ///
+ public string PipeName { get; }
+
+ /// Adds a local object exposing RPC methods callable by clients.
+ /// An arbitrary service object that will be introspected to add to RPC.
+ public void AddService(object service) => this.registry.AddService(service);
+
+ ///
+ /// Adds a standalone JSON-RPC method callable by clients.
+ ///
+ /// The name to add.
+ /// The delegate that acts as the handler.
+ public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler);
+
+ /// Starts accepting client connections.
+ public void Start()
+ {
+ if (this.acceptLoopTask != null) return;
+ this.acceptLoopTask = Task.Run(this.AcceptLoopAsync);
+ }
+
+ /// Invoke an RPC request on a specific client expecting a result.
+ /// The client ID to invoke.
+ /// The method to invoke.
+ /// Any arguments to invoke.
+ /// An optional return based on the specified RPC.
+ /// The expected response type.
+ public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments)
+ {
+ if (!this.sessions.TryGetValue(clientId, out var session))
+ throw new KeyNotFoundException($"No client {clientId}");
+
+ return session.Rpc.InvokeAsync(method, arguments);
+ }
+
+ /// Send a notification to all connected clients (no response expected).
+ /// The method name to broadcast.
+ /// The arguments to broadcast.
+ /// Returns a Task when completed.
+ public Task BroadcastNotifyAsync(string method, params object[] arguments)
+ {
+ var list = this.sessions.Values;
+ var tasks = new List(list.Count);
+ foreach (var s in list)
+ {
+ tasks.Add(s.Rpc.NotifyAsync(method, arguments));
+ }
+
+ return Task.WhenAll(tasks);
+ }
+
+ ///
+ /// Gets a list of connected client IDs.
+ ///
+ /// Connected client IDs.
+ public IReadOnlyCollection GetClientIds() => this.sessions.Keys.AsReadOnlyCollection();
+
+ ///
+ public void Dispose()
+ {
+ this.cts.Cancel();
+ this.acceptLoopTask?.Wait(1000);
+
+ foreach (var kv in this.sessions)
+ {
+ kv.Value.Dispose();
+ }
+
+ this.sessions.Clear();
+ this.cts.Dispose();
+ this.log.Information("PipeRpcHost disposed ({Pipe})", this.PipeName);
+ GC.SuppressFinalize(this);
+ }
+
+ private PipeSecurity BuildPipeSecurity()
+ {
+ var ps = new PipeSecurity();
+ ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User!, PipeAccessRights.FullControl, AccessControlType.Allow));
+
+ return ps;
+ }
+
+ private async Task AcceptLoopAsync()
+ {
+ this.log.Information("PipeRpcHost starting on pipe {Pipe}", this.PipeName);
+ var token = this.cts.Token;
+ var security = this.BuildPipeSecurity();
+
+ while (!token.IsCancellationRequested)
+ {
+ NamedPipeServerStream? server = null;
+ try
+ {
+ server = NamedPipeServerStreamAcl.Create(
+ this.PipeName,
+ PipeDirection.InOut,
+ NamedPipeServerStream.MaxAllowedServerInstances,
+ PipeTransmissionMode.Message,
+ PipeOptions.Asynchronous,
+ 65536,
+ 65536,
+ security);
+
+ await server.WaitForConnectionAsync(token).ConfigureAwait(false);
+
+ var session = new RpcConnection(server, this.registry);
+ this.sessions.TryAdd(session.Id, session);
+
+ this.log.Debug("RPC connection created: {Id}", session.Id);
+
+ _ = session.Completion.ContinueWith(t =>
+ {
+ this.sessions.TryRemove(session.Id, out _);
+ this.log.Debug("RPC connection removed: {Id}", session.Id);
+ }, TaskScheduler.Default);
+ }
+ catch (OperationCanceledException)
+ {
+ server?.Dispose();
+ break;
+ }
+ catch (Exception ex)
+ {
+ server?.Dispose();
+ this.log.Error(ex, "Error in pipe accept loop");
+ await Task.Delay(500, token).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs b/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs
new file mode 100644
index 000000000..8e1c3a085
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs
@@ -0,0 +1,92 @@
+using System.IO.Pipes;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Serilog;
+using StreamJsonRpc;
+
+namespace Dalamud.Networking.Pipes.Rpc;
+
+///
+/// A single RPC client session connected via named pipe.
+///
+internal class RpcConnection : IDisposable
+{
+ private readonly NamedPipeServerStream pipe;
+ private readonly RpcServiceRegistry registry;
+ private readonly CancellationTokenSource cts = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The named pipe that this connection will handle.
+ /// A registry of RPC services.
+ public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry)
+ {
+ this.Id = Guid.CreateVersion7();
+ this.pipe = pipe;
+ this.registry = registry;
+
+ var formatter = new JsonMessageFormatter();
+ var handler = new HeaderDelimitedMessageHandler(pipe, pipe, formatter);
+
+ this.Rpc = new JsonRpc(handler);
+ this.Rpc.AllowModificationWhileListening = true;
+ this.Rpc.Disconnected += this.OnDisconnected;
+ this.registry.Attach(this.Rpc);
+
+ this.Rpc.StartListening();
+ }
+
+ ///
+ /// Gets the GUID for this connection.
+ ///
+ public Guid Id { get; }
+
+ ///
+ /// Gets the JsonRpc instance for this connection.
+ ///
+ public JsonRpc Rpc { get; }
+
+ ///
+ /// Gets a task that's called on RPC completion.
+ ///
+ public Task Completion => this.Rpc.Completion;
+
+ ///
+ public void Dispose()
+ {
+ if (!this.cts.IsCancellationRequested)
+ {
+ this.cts.Cancel();
+ }
+
+ try
+ {
+ this.Rpc.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Error disposing JsonRpc for client {Id}", this.Id);
+ }
+
+ try
+ {
+ this.pipe.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Error disposing pipe for client {Id}", this.Id);
+ }
+
+ this.cts.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ private void OnDisconnected(object? sender, JsonRpcDisconnectedEventArgs e)
+ {
+ Log.Debug("RPC client {Id} disconnected: {Reason}", this.Id, e.Description);
+ this.registry.Detach(this.Rpc);
+ this.Dispose();
+ }
+}
diff --git a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs
new file mode 100644
index 000000000..78df27323
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs
@@ -0,0 +1,49 @@
+using Dalamud.Logging.Internal;
+
+namespace Dalamud.Networking.Pipes.Rpc;
+
+///
+/// The Dalamud service repsonsible for hosting the RPC.
+///
+[ServiceManager.EarlyLoadedService]
+internal class RpcHostService : IServiceType, IInternalDisposableService
+{
+ private readonly ModuleLog log = new("RPC");
+ private readonly PipeRpcHost host;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [ServiceManager.ServiceConstructor]
+ public RpcHostService()
+ {
+ this.host = new PipeRpcHost();
+ this.host.Start();
+
+ this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName);
+ }
+
+ ///
+ /// Gets the RPC host to drill down.
+ ///
+ public PipeRpcHost Host => this.host;
+
+ ///
+ /// Add a new service Object to the RPC host.
+ ///
+ /// The object to add.
+ public void AddService(object service) => this.host.AddService(service);
+
+ ///
+ /// Add a new standalone method to the RPC host.
+ ///
+ /// The method name to add.
+ /// The handler to add.
+ public void AddMethod(string name, Delegate handler) => this.host.AddMethod(name, handler);
+
+ ///
+ public void DisposeService()
+ {
+ this.host.Dispose();
+ }
+}
diff --git a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs
new file mode 100644
index 000000000..71037d45e
--- /dev/null
+++ b/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+using System.Threading;
+
+using StreamJsonRpc;
+
+namespace Dalamud.Networking.Pipes.Rpc;
+
+///
+/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session.
+/// New sessions get all previously registered targets; newly added targets are attached to all active sessions.
+///
+internal class RpcServiceRegistry
+{
+ private readonly Lock sync = new();
+ private readonly List
+
+
@@ -22,26 +24,35 @@
+
+
+
+
+
+
+
+
+
@@ -54,4 +65,4 @@
-
\ No newline at end of file
+
From 4937a2f4bd2e551669e7d158b44d0f6e681ffc1d Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Sun, 16 Nov 2025 18:14:02 -0800
Subject: [PATCH 027/201] CR changes
---
.../Networking/Pipes/Api/PluginLinkHandler.cs | 4 ++-
.../Pipes/Internal/LinkHandlerService.cs | 36 ++++---------------
Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs | 2 +-
Dalamud/Plugin/Services/IPluginLinkHandler.cs | 5 ++-
4 files changed, 15 insertions(+), 32 deletions(-)
diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
index 2c99901b4..d8f43907c 100644
--- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
+++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
@@ -1,5 +1,6 @@
using System.Linq;
+using Dalamud.Console;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Networking.Pipes.Internal;
@@ -43,7 +44,8 @@ public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler
private void HandleUri(DalamudUri uri)
{
var target = uri.Path.Split("/").FirstOrDefault();
- if (target == null || !string.Equals(target, this.localPlugin.InternalName, StringComparison.OrdinalIgnoreCase))
+ var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName);
+ if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase))
{
return;
}
diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
index 79bb1e017..3cc4af9f4 100644
--- a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
+++ b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Dalamud.Logging.Internal;
using Dalamud.Networking.Pipes.Rpc;
+using Dalamud.Utility;
namespace Dalamud.Networking.Pipes.Internal;
@@ -65,10 +66,7 @@ internal class LinkHandlerService : IInternalDisposableService
if (!this.handlers.TryGetValue(ns, out var list))
return;
- lock (list)
- {
- list.RemoveAll(x => x == handler);
- }
+ list.RemoveAll(x => x == handler);
if (list.Count == 0)
this.handlers.TryRemove(ns, out _);
@@ -85,25 +83,12 @@ internal class LinkHandlerService : IInternalDisposableService
this.log.Information("Received URI: {Uri}", uri.ToString());
var ns = uri.Namespace;
- if (!this.handlers.TryGetValue(ns, out var list))
+ if (!this.handlers.TryGetValue(ns, out var actions))
return;
- Action[] snapshot;
- lock (list)
+ foreach (var h in actions)
{
- snapshot = list.ToArray();
- }
-
- foreach (var h in snapshot)
- {
- try
- {
- h(uri);
- }
- catch (Exception e)
- {
- this.log.Warning(e, "Link handler threw for {UriPath}", uri.Path);
- }
+ h.InvokeSafely(uri);
}
}
@@ -116,14 +101,7 @@ internal class LinkHandlerService : IInternalDisposableService
if (string.IsNullOrWhiteSpace(uri))
return;
- try
- {
- var du = DalamudUri.FromUri(uri);
- this.Dispatch(du);
- }
- catch (Exception)
- {
- // swallow parse errors; clients shouldn't crash the host
- }
+ var du = DalamudUri.FromUri(uri);
+ this.Dispatch(du);
}
}
diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
index 07dc9d96a..ad1cc72cd 100644
--- a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
+++ b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
@@ -53,7 +53,7 @@ internal class PipeRpcHost : IDisposable
public void Start()
{
if (this.acceptLoopTask != null) return;
- this.acceptLoopTask = Task.Run(this.AcceptLoopAsync);
+ this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning);
}
/// Invoke an RPC request on a specific client expecting a result.
diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
index 57f772768..22139814d 100644
--- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs
+++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
@@ -1,4 +1,6 @@
-using Dalamud.Networking.Pipes;
+using System.Diagnostics.CodeAnalysis;
+
+using Dalamud.Networking.Pipes;
namespace Dalamud.Plugin.Services;
@@ -6,6 +8,7 @@ namespace Dalamud.Plugin.Services;
/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the
/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace.
///
+[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")]
public interface IPluginLinkHandler
{
///
From 19a3926051ce6aa30ac907a5fb7201536b971452 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Sun, 16 Nov 2025 21:35:33 -0800
Subject: [PATCH 028/201] Better hello message
---
.../Networking/Pipes/Api/PluginLinkHandler.cs | 1 +
.../Pipes/Internal/ClientHelloService.cs | 45 +++++++++++++++----
2 files changed, 37 insertions(+), 9 deletions(-)
diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
index d8f43907c..78fbb0d82 100644
--- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
+++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
@@ -6,6 +6,7 @@ using Dalamud.IoC.Internal;
using Dalamud.Networking.Pipes.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
+#pragma warning disable DAL_RPC
namespace Dalamud.Networking.Pipes.Api;
diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
index cc06560bd..9c182561e 100644
--- a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
+++ b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
@@ -1,10 +1,13 @@
using System.Threading.Tasks;
+using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Networking.Pipes.Rpc;
using Dalamud.Utility;
+using Lumina.Excel.Sheets;
+
namespace Dalamud.Networking.Pipes.Internal;
///
@@ -30,25 +33,49 @@ internal sealed class ClientHelloService : IInternalDisposableService
/// Respond with information.
public async Task HandleHello(ClientHelloRequest request)
{
- var framework = await Service.GetAsync();
var dalamud = await Service.GetAsync();
- var clientState = await Service.GetAsync();
- var response = await framework.RunOnFrameworkThread(() => new ClientHelloResponse
+ return new ClientHelloResponse
{
ApiVersion = "1.0",
DalamudVersion = Util.GetScmVersion(),
GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown",
- PlayerName = clientState.IsLoggedIn ? clientState.LocalPlayer?.Name.ToString() ?? "Unknown" : null,
- });
-
- return response;
+ ClientIdentifier = await this.GetClientIdentifier(),
+ };
}
///
public void DisposeService()
{
}
+
+ private async Task GetClientIdentifier()
+ {
+ var framework = await Service.GetAsync();
+ var clientState = await Service.GetAsync();
+ var dataManager = await Service.GetAsync();
+
+ var clientIdentifier = $"FFXIV Process ${Environment.ProcessId}";
+
+ await framework.RunOnFrameworkThread(() =>
+ {
+ if (clientState.IsLoggedIn)
+ {
+ var player = clientState.LocalPlayer;
+ if (player != null)
+ {
+ var world = dataManager.GetExcelSheet().GetRow(player.HomeWorld.RowId);
+ clientIdentifier = $"Logged in as {player.Name.TextValue} @ {world.Name.ExtractText()}";
+ }
+ }
+ else
+ {
+ clientIdentifier = "On login screen";
+ }
+ });
+
+ return clientIdentifier;
+ }
}
///
@@ -88,7 +115,7 @@ internal record ClientHelloResponse
public string? GameVersion { get; init; }
///
- /// Gets or sets the player name, or null if the player isn't logged in.
+ /// Gets an identifier for this client.
///
- public string? PlayerName { get; set; }
+ public string? ClientIdentifier { get; init; }
}
From cc9191657453986cb6451f63f3238b3b29b7e26c Mon Sep 17 00:00:00 2001
From: goaaats
Date: Tue, 18 Nov 2025 00:52:30 +0100
Subject: [PATCH 029/201] Fix bad merge
---
Dalamud/Game/ClientState/Buddy/BuddyList.cs | 6 ++----
Dalamud/Game/ClientState/Party/PartyList.cs | 4 ++--
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
index a76b520af..4d5fc2aab 100644
--- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs
+++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
@@ -9,8 +9,6 @@ using Dalamud.Plugin.Services;
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
-
-using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
namespace Dalamud.Game.ClientState.Buddy;
@@ -74,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
}
- private unsafe CSBuddy* BuddyListStruct => &UIState.Instance()->Buddy;
+ private unsafe CSBuddy* BuddyListStruct => &CSUIState.Instance()->Buddy;
///
public IBuddyMember? this[int index]
@@ -113,7 +111,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
if (address == 0)
return null;
- if (this.clientState.LocalContentId == 0)
+ if (this.playerState.ContentId == 0)
return null;
var buddy = new BuddyMember((CSBuddyMember*)address);
diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs
index 1a5177b10..1dede1dd3 100644
--- a/Dalamud/Game/ClientState/Party/PartyList.cs
+++ b/Dalamud/Game/ClientState/Party/PartyList.cs
@@ -93,7 +93,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
///
public IPartyMember? CreatePartyMemberReference(nint address)
{
- if (this.clientState.LocalContentId == 0)
+ if (this.playerState.ContentId == 0)
return null;
if (address == 0)
@@ -114,7 +114,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
///
public IPartyMember? CreateAllianceMemberReference(nint address)
{
- if (this.clientState.LocalContentId == 0)
+ if (this.playerState.ContentId == 0)
return null;
if (address == 0)
From 6a69a6e197ac6761d6c3d39fe3a879c852151cf6 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Tue, 18 Nov 2025 00:58:08 +0100
Subject: [PATCH 030/201] Fix some warnings
---
Dalamud/Game/ClientState/Buddy/BuddyList.cs | 2 +-
Dalamud/GlobalSuppressions.cs | 1 +
Dalamud/Networking/Pipes/DalamudUri.cs | 34 +++++++++----------
Dalamud/Plugin/Services/IPluginLinkHandler.cs | 5 +--
4 files changed, 22 insertions(+), 20 deletions(-)
diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
index 4d5fc2aab..b8e4c0fcc 100644
--- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs
+++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
@@ -8,8 +8,8 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
-using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
+using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
namespace Dalamud.Game.ClientState.Buddy;
diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs
index 8a9d31b12..35754eb04 100644
--- a/Dalamud/GlobalSuppressions.cs
+++ b/Dalamud/GlobalSuppressions.cs
@@ -21,6 +21,7 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments", Justification = "I like having comments in blocks")]
// ImRAII stuff
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")]
diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Pipes/DalamudUri.cs
index 03ad15af1..7e639cbbe 100644
--- a/Dalamud/Networking/Pipes/DalamudUri.cs
+++ b/Dalamud/Networking/Pipes/DalamudUri.cs
@@ -61,6 +61,23 @@ public record DalamudUri
///
public override string ToString() => this.rawUri.ToString();
+ ///
+ /// Build a DalamudURI from a given URI.
+ ///
+ /// The URI to convert to a Dalamud URI.
+ /// Returns a DalamudUri.
+ public static DalamudUri FromUri(Uri uri)
+ {
+ return new DalamudUri(uri);
+ }
+
+ ///
+ /// Build a DalamudURI from a URI in string format.
+ ///
+ /// The URI to convert to a Dalamud URI.
+ /// Returns a DalamudUri.
+ public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri));
+
private string[] GetDataSegments()
{
// reimplementation of the System.URI#Segments, under MIT license.
@@ -82,21 +99,4 @@ public record DalamudUri
return segments.ToArray();
}
-
- ///
- /// Build a DalamudURI from a given URI.
- ///
- /// The URI to convert to a Dalamud URI.
- /// Returns a DalamudUri.
- public static DalamudUri FromUri(Uri uri)
- {
- return new DalamudUri(uri);
- }
-
- ///
- /// Build a DalamudURI from a URI in string format.
- ///
- /// The URI to convert to a Dalamud URI.
- /// Returns a DalamudUri.
- public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri));
}
diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
index 22139814d..5d2d32728 100644
--- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs
+++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
@@ -6,7 +6,7 @@ namespace Dalamud.Plugin.Services;
///
/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the
-/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace.
+/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace.
///
[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")]
public interface IPluginLinkHandler
@@ -14,7 +14,8 @@ public interface IPluginLinkHandler
///
/// A delegate containing the received URI.
///
- delegate void PluginUriReceived(DalamudUri uri);
+ /// The URI opened by the user.
+ public delegate void PluginUriReceived(DalamudUri uri);
///
/// The event fired when a URI targeting this plugin is received.
From 71927a8bf6fc0135e59fbd2e9e515aacb5e7d75f Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Tue, 18 Nov 2025 15:18:16 -0800
Subject: [PATCH 031/201] feat: Add unix sockets
- Unix sockets run parallel to Named Pipes
- Named Pipes will only run on non-Wine
- If the game crashes, the next run will clean up an orphaned socket.
- Restructure RPC to be a bit tidier
---
.../{Pipes => Rpc}/DalamudUriTests.cs | 5 +-
.../Networking/Pipes/Rpc/RpcHostService.cs | 49 ----
.../{Pipes => Rpc}/Api/PluginLinkHandler.cs | 6 +-
.../{Pipes => Rpc/Model}/DalamudUri.cs | 2 +-
.../{Pipes => }/Rpc/RpcConnection.cs | 23 +-
Dalamud/Networking/Rpc/RpcHostService.cs | 105 +++++++++
.../{Pipes => }/Rpc/RpcServiceRegistry.cs | 2 +-
.../Service}/ClientHelloService.cs | 3 +-
.../Service}/LinkHandlerService.cs | 4 +-
.../Networking/Rpc/Transport/IRpcTransport.cs | 32 +++
.../Transport/PipeRpcTransport.cs} | 30 +--
.../Rpc/Transport/UnixRpcTransport.cs | 223 ++++++++++++++++++
Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +-
Dalamud/Utility/UnixSocketUtil.cs | 92 ++++++++
14 files changed, 487 insertions(+), 91 deletions(-)
rename Dalamud.Test/{Pipes => Rpc}/DalamudUriTests.cs (98%)
delete mode 100644 Dalamud/Networking/Pipes/Rpc/RpcHostService.cs
rename Dalamud/Networking/{Pipes => Rpc}/Api/PluginLinkHandler.cs (93%)
rename Dalamud/Networking/{Pipes => Rpc/Model}/DalamudUri.cs (98%)
rename Dalamud/Networking/{Pipes => }/Rpc/RpcConnection.cs (76%)
create mode 100644 Dalamud/Networking/Rpc/RpcHostService.cs
rename Dalamud/Networking/{Pipes => }/Rpc/RpcServiceRegistry.cs (98%)
rename Dalamud/Networking/{Pipes/Internal => Rpc/Service}/ClientHelloService.cs (97%)
rename Dalamud/Networking/{Pipes/Internal => Rpc/Service}/LinkHandlerService.cs (97%)
create mode 100644 Dalamud/Networking/Rpc/Transport/IRpcTransport.cs
rename Dalamud/Networking/{Pipes/Rpc/PipeRpcHost.cs => Rpc/Transport/PipeRpcTransport.cs} (81%)
create mode 100644 Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
create mode 100644 Dalamud/Utility/UnixSocketUtil.cs
diff --git a/Dalamud.Test/Pipes/DalamudUriTests.cs b/Dalamud.Test/Rpc/DalamudUriTests.cs
similarity index 98%
rename from Dalamud.Test/Pipes/DalamudUriTests.cs
rename to Dalamud.Test/Rpc/DalamudUriTests.cs
index 4977f3814..b371a5698 100644
--- a/Dalamud.Test/Pipes/DalamudUriTests.cs
+++ b/Dalamud.Test/Rpc/DalamudUriTests.cs
@@ -1,10 +1,11 @@
using System;
using System.Linq;
-using Dalamud.Networking.Pipes;
+using Dalamud.Networking.Rpc.Model;
+
using Xunit;
-namespace Dalamud.Test.Pipes
+namespace Dalamud.Test.Rpc
{
public class DalamudUriTests
{
diff --git a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs
deleted file mode 100644
index 78df27323..000000000
--- a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using Dalamud.Logging.Internal;
-
-namespace Dalamud.Networking.Pipes.Rpc;
-
-///
-/// The Dalamud service repsonsible for hosting the RPC.
-///
-[ServiceManager.EarlyLoadedService]
-internal class RpcHostService : IServiceType, IInternalDisposableService
-{
- private readonly ModuleLog log = new("RPC");
- private readonly PipeRpcHost host;
-
- ///
- /// Initializes a new instance of the class.
- ///
- [ServiceManager.ServiceConstructor]
- public RpcHostService()
- {
- this.host = new PipeRpcHost();
- this.host.Start();
-
- this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName);
- }
-
- ///
- /// Gets the RPC host to drill down.
- ///
- public PipeRpcHost Host => this.host;
-
- ///
- /// Add a new service Object to the RPC host.
- ///
- /// The object to add.
- public void AddService(object service) => this.host.AddService(service);
-
- ///
- /// Add a new standalone method to the RPC host.
- ///
- /// The method name to add.
- /// The handler to add.
- public void AddMethod(string name, Delegate handler) => this.host.AddMethod(name, handler);
-
- ///
- public void DisposeService()
- {
- this.host.Dispose();
- }
-}
diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs
similarity index 93%
rename from Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
rename to Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs
index 78fbb0d82..e9372bf0e 100644
--- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
+++ b/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs
@@ -3,12 +3,14 @@
using Dalamud.Console;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
-using Dalamud.Networking.Pipes.Internal;
+using Dalamud.Networking.Rpc.Model;
+using Dalamud.Networking.Rpc.Service;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
+
#pragma warning disable DAL_RPC
-namespace Dalamud.Networking.Pipes.Api;
+namespace Dalamud.Networking.Rpc.Api;
///
[PluginInterface]
diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Rpc/Model/DalamudUri.cs
similarity index 98%
rename from Dalamud/Networking/Pipes/DalamudUri.cs
rename to Dalamud/Networking/Rpc/Model/DalamudUri.cs
index 7e639cbbe..852478762 100644
--- a/Dalamud/Networking/Pipes/DalamudUri.cs
+++ b/Dalamud/Networking/Rpc/Model/DalamudUri.cs
@@ -2,7 +2,7 @@
using System.Collections.Specialized;
using System.Web;
-namespace Dalamud.Networking.Pipes;
+namespace Dalamud.Networking.Rpc.Model;
///
/// A Dalamud Uri, in the format:
diff --git a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs b/Dalamud/Networking/Rpc/RpcConnection.cs
similarity index 76%
rename from Dalamud/Networking/Pipes/Rpc/RpcConnection.cs
rename to Dalamud/Networking/Rpc/RpcConnection.cs
index 8e1c3a085..5288948eb 100644
--- a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs
+++ b/Dalamud/Networking/Rpc/RpcConnection.cs
@@ -1,34 +1,37 @@
-using System.IO.Pipes;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Dalamud.Networking.Rpc.Service;
+
using Serilog;
+
using StreamJsonRpc;
-namespace Dalamud.Networking.Pipes.Rpc;
+namespace Dalamud.Networking.Rpc;
///
-/// A single RPC client session connected via named pipe.
+/// A single RPC client session connected via a stream (named pipe or Unix socket).
///
internal class RpcConnection : IDisposable
{
- private readonly NamedPipeServerStream pipe;
+ private readonly Stream stream;
private readonly RpcServiceRegistry registry;
private readonly CancellationTokenSource cts = new();
///
/// Initializes a new instance of the class.
///
- /// The named pipe that this connection will handle.
+ /// The stream that this connection will handle.
/// A registry of RPC services.
- public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry)
+ public RpcConnection(Stream stream, RpcServiceRegistry registry)
{
this.Id = Guid.CreateVersion7();
- this.pipe = pipe;
+ this.stream = stream;
this.registry = registry;
var formatter = new JsonMessageFormatter();
- var handler = new HeaderDelimitedMessageHandler(pipe, pipe, formatter);
+ var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter);
this.Rpc = new JsonRpc(handler);
this.Rpc.AllowModificationWhileListening = true;
@@ -72,11 +75,11 @@ internal class RpcConnection : IDisposable
try
{
- this.pipe.Dispose();
+ this.stream.Dispose();
}
catch (Exception ex)
{
- Log.Debug(ex, "Error disposing pipe for client {Id}", this.Id);
+ Log.Debug(ex, "Error disposing stream for client {Id}", this.Id);
}
this.cts.Dispose();
diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs
new file mode 100644
index 000000000..f164992eb
--- /dev/null
+++ b/Dalamud/Networking/Rpc/RpcHostService.cs
@@ -0,0 +1,105 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+
+using Dalamud.Logging.Internal;
+using Dalamud.Networking.Rpc.Transport;
+using Dalamud.Utility;
+
+namespace Dalamud.Networking.Rpc;
+
+///
+/// The Dalamud service repsonsible for hosting the RPC.
+///
+[ServiceManager.EarlyLoadedService]
+internal class RpcHostService : IServiceType, IInternalDisposableService
+{
+ private readonly ModuleLog log = new("RPC");
+ private readonly RpcServiceRegistry registry = new();
+ private readonly List transports = [];
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [ServiceManager.ServiceConstructor]
+ public RpcHostService()
+ {
+ this.StartUnixTransport();
+ this.StartPipeTransport();
+
+ if (this.transports.Count == 0)
+ {
+ this.log.Warning("No RPC hosts could be started on this platform");
+ }
+ }
+
+ ///
+ /// Gets all active RPC transports.
+ ///
+ public IReadOnlyList Transports => this.transports;
+
+ ///
+ /// Add a new service Object to the RPC host.
+ ///
+ /// The object to add.
+ public void AddService(object service) => this.registry.AddService(service);
+
+ ///
+ /// Add a new standalone method to the RPC host.
+ ///
+ /// The method name to add.
+ /// The handler to add.
+ public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler);
+
+ ///
+ public void DisposeService()
+ {
+ foreach (var host in this.transports)
+ {
+ host.Dispose();
+ }
+
+ this.transports.Clear();
+ }
+
+ ///
+ public async Task InvokeClientAsync(Guid clientId, string method, params object[] arguments)
+ {
+ var clients = this.transports.SelectMany(t => t.Connections).ToImmutableDictionary();
+
+ if (!clients.TryGetValue(clientId, out var session))
+ throw new KeyNotFoundException($"No client {clientId}");
+
+ return await session.Rpc.InvokeAsync(method, arguments).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task BroadcastNotifyAsync(string method, params object[] arguments)
+ {
+ await foreach (var transport in this.transports.ToAsyncEnumerable().ConfigureAwait(false))
+ {
+ await transport.BroadcastNotifyAsync(method, arguments).ConfigureAwait(false);
+ }
+ }
+
+ private void StartUnixTransport()
+ {
+ var transport = new UnixRpcTransport(this.registry);
+ this.transports.Add(transport);
+ transport.Start();
+ this.log.Information("RpcHostService started Unix socket host: {Socket}", transport.SocketPath);
+ }
+
+ private void StartPipeTransport()
+ {
+ // Wine doesn't support named pipes.
+ if (Util.IsWine())
+ return;
+
+ var transport = new PipeRpcTransport(this.registry);
+ this.transports.Add(transport);
+ transport.Start();
+ this.log.Information("RpcHostService started named pipe host: {Pipe}", transport.PipeName);
+ }
+}
diff --git a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs
similarity index 98%
rename from Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs
rename to Dalamud/Networking/Rpc/RpcServiceRegistry.cs
index 71037d45e..6daea14bf 100644
--- a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs
+++ b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs
@@ -3,7 +3,7 @@ using System.Threading;
using StreamJsonRpc;
-namespace Dalamud.Networking.Pipes.Rpc;
+namespace Dalamud.Networking.Rpc;
///
/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session.
diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs
similarity index 97%
rename from Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
rename to Dalamud/Networking/Rpc/Service/ClientHelloService.cs
index 9c182561e..041bc135f 100644
--- a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
+++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs
@@ -3,12 +3,11 @@
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
-using Dalamud.Networking.Pipes.Rpc;
using Dalamud.Utility;
using Lumina.Excel.Sheets;
-namespace Dalamud.Networking.Pipes.Internal;
+namespace Dalamud.Networking.Rpc.Service;
///
/// A minimal service to respond with information about this client.
diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs
similarity index 97%
rename from Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
rename to Dalamud/Networking/Rpc/Service/LinkHandlerService.cs
index 3cc4af9f4..9fa311ede 100644
--- a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
+++ b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs
@@ -2,10 +2,10 @@
using System.Collections.Generic;
using Dalamud.Logging.Internal;
-using Dalamud.Networking.Pipes.Rpc;
+using Dalamud.Networking.Rpc.Model;
using Dalamud.Utility;
-namespace Dalamud.Networking.Pipes.Internal;
+namespace Dalamud.Networking.Rpc.Service;
///
/// A service responsible for handling Dalamud URIs and dispatching them accordingly.
diff --git a/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs
new file mode 100644
index 000000000..ad7578eb4
--- /dev/null
+++ b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Dalamud.Networking.Rpc.Transport;
+
+///
+/// Interface for RPC host implementations (named pipes or Unix sockets).
+///
+internal interface IRpcTransport : IDisposable
+{
+ ///
+ /// Gets a list of active RPC connections.
+ ///
+ IReadOnlyDictionary Connections { get; }
+
+ /// Starts accepting client connections.
+ void Start();
+
+ /// Invoke an RPC request on a specific client expecting a result.
+ /// The client ID to invoke.
+ /// The method to invoke.
+ /// Any arguments to invoke.
+ /// An optional return based on the specified RPC.
+ /// The expected response type.
+ Task InvokeClientAsync(Guid clientId, string method, params object[] arguments);
+
+ /// Send a notification to all connected clients (no response expected).
+ /// The method name to broadcast.
+ /// The arguments to broadcast.
+ /// Returns a Task when completed.
+ Task BroadcastNotifyAsync(string method, params object[] arguments);
+}
diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs
similarity index 81%
rename from Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
rename to Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs
index ad1cc72cd..0cefeb853 100644
--- a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
+++ b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs
@@ -9,26 +9,28 @@ using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
-namespace Dalamud.Networking.Pipes.Rpc;
+namespace Dalamud.Networking.Rpc.Transport;
///
/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc.
///
-internal class PipeRpcHost : IDisposable
+internal class PipeRpcTransport : IRpcTransport
{
private readonly ModuleLog log = new("RPC/Host");
- private readonly RpcServiceRegistry registry = new();
+ private readonly RpcServiceRegistry registry;
private readonly CancellationTokenSource cts = new();
private readonly ConcurrentDictionary sessions = new();
private Task? acceptLoopTask;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
+ /// The RPC service registry to use.
/// The pipe name to create.
- public PipeRpcHost(string? pipeName = null)
+ public PipeRpcTransport(RpcServiceRegistry registry, string? pipeName = null)
{
+ this.registry = registry;
// Default pipe name based on current process ID for uniqueness per Dalamud instance.
this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}";
}
@@ -38,16 +40,8 @@ internal class PipeRpcHost : IDisposable
///
public string PipeName { get; }
- /// Adds a local object exposing RPC methods callable by clients.
- /// An arbitrary service object that will be introspected to add to RPC.
- public void AddService(object service) => this.registry.AddService(service);
-
- ///
- /// Adds a standalone JSON-RPC method callable by clients.
- ///
- /// The name to add.
- /// The delegate that acts as the handler.
- public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler);
+ ///
+ public IReadOnlyDictionary Connections => this.sessions;
/// Starts accepting client connections.
public void Start()
@@ -86,12 +80,6 @@ internal class PipeRpcHost : IDisposable
return Task.WhenAll(tasks);
}
- ///
- /// Gets a list of connected client IDs.
- ///
- /// Connected client IDs.
- public IReadOnlyCollection GetClientIds() => this.sessions.Keys.AsReadOnlyCollection();
-
///
public void Dispose()
{
diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
new file mode 100644
index 000000000..3019f5aaf
--- /dev/null
+++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
@@ -0,0 +1,223 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Logging.Internal;
+using Dalamud.Utility;
+
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Networking.Rpc.Transport;
+
+///
+/// Simple multi-client JSON-RPC Unix socket host using StreamJsonRpc.
+///
+internal class UnixRpcTransport : IRpcTransport
+{
+ private readonly ModuleLog log = new("RPC/UnixHost");
+
+ private readonly RpcServiceRegistry registry;
+ private readonly CancellationTokenSource cts = new();
+ private readonly ConcurrentDictionary sessions = new();
+ private readonly string? cleanupSocketDirectory;
+
+ private Task? acceptLoopTask;
+ private Socket? listenSocket;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The RPC service registry to use.
+ /// The Unix socket path to create. If null, defaults to a path based on process ID.
+ public UnixRpcTransport(RpcServiceRegistry registry, string? socketPath = null)
+ {
+ this.registry = registry;
+
+ if (socketPath != null)
+ {
+ this.SocketPath = socketPath;
+ }
+ else
+ {
+ var dalamudConfigPath = Service.Get().StartInfo.ConfigurationPath;
+ var dalamudHome = Path.GetDirectoryName(dalamudConfigPath);
+ var socketName = $"DalamudRPC.{Environment.ProcessId}.sock";
+
+ if (dalamudHome == null)
+ {
+ this.SocketPath = Path.Combine(Path.GetTempPath(), socketName);
+ this.log.Warning("Dalamud home is empty! UDS socket will be in temp.");
+ }
+ else
+ {
+ this.SocketPath = Path.Combine(dalamudHome, socketName);
+ this.cleanupSocketDirectory = dalamudHome;
+ }
+ }
+ }
+
+ ///
+ /// Gets the path of the Unix socket this RPC host is using.
+ ///
+ public string SocketPath { get; }
+
+ ///
+ public IReadOnlyDictionary Connections => this.sessions;
+
+ /// Starts accepting client connections.
+ public void Start()
+ {
+ if (this.acceptLoopTask != null) return;
+
+ // Make the directory for the socket if it doesn't exist
+ var socketDir = Path.GetDirectoryName(this.SocketPath);
+ if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir))
+ {
+ try
+ {
+ Directory.CreateDirectory(socketDir);
+ }
+ catch (Exception ex)
+ {
+ this.log.Error(ex, "Failed to create socket directory: {Path}", socketDir);
+ return;
+ }
+ }
+
+ // Delete existing socket for this PID, if it exists.
+ if (File.Exists(this.SocketPath))
+ {
+ try
+ {
+ File.Delete(this.SocketPath);
+ }
+ catch (Exception ex)
+ {
+ this.log.Warning(ex, "Failed to delete existing socket file: {Path}", this.SocketPath);
+ }
+ }
+
+ this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning);
+
+ // note: needs to be run _after_ we're alive so that we don't delete our own socket.
+ if (this.cleanupSocketDirectory != null)
+ {
+ Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory));
+ }
+ }
+
+ /// Invoke an RPC request on a specific client expecting a result.
+ /// The client ID to invoke.
+ /// The method to invoke.
+ /// Any arguments to invoke.
+ /// An optional return based on the specified RPC.
+ /// The expected response type.
+ public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments)
+ {
+ if (!this.sessions.TryGetValue(clientId, out var session))
+ throw new KeyNotFoundException($"No client {clientId}");
+
+ return session.Rpc.InvokeAsync(method, arguments);
+ }
+
+ /// Send a notification to all connected clients (no response expected).
+ /// The method name to broadcast.
+ /// The arguments to broadcast.
+ /// Returns a Task when completed.
+ public Task BroadcastNotifyAsync(string method, params object[] arguments)
+ {
+ var list = this.sessions.Values;
+ var tasks = new List(list.Count);
+ foreach (var s in list)
+ {
+ tasks.Add(s.Rpc.NotifyAsync(method, arguments));
+ }
+
+ return Task.WhenAll(tasks);
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.cts.Cancel();
+ this.acceptLoopTask?.Wait(1000);
+
+ foreach (var kv in this.sessions)
+ {
+ kv.Value.Dispose();
+ }
+
+ this.sessions.Clear();
+
+ this.listenSocket?.Dispose();
+
+ if (File.Exists(this.SocketPath))
+ {
+ try
+ {
+ File.Delete(this.SocketPath);
+ }
+ catch (Exception ex)
+ {
+ this.log.Warning(ex, "Failed to delete socket file on dispose: {Path}", this.SocketPath);
+ }
+ }
+
+ this.cts.Dispose();
+ this.log.Information("UnixRpcHost disposed ({Socket})", this.SocketPath);
+ GC.SuppressFinalize(this);
+ }
+
+ private async Task AcceptLoopAsync()
+ {
+ this.log.Information("UnixRpcHost starting on socket {Socket}", this.SocketPath);
+ var token = this.cts.Token;
+
+ try
+ {
+ var endpoint = new UnixDomainSocketEndPoint(this.SocketPath);
+ this.listenSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
+ this.listenSocket.Bind(endpoint);
+ this.listenSocket.Listen(128);
+
+ while (!token.IsCancellationRequested)
+ {
+ Socket? clientSocket = null;
+ try
+ {
+ clientSocket = await this.listenSocket.AcceptAsync(token).ConfigureAwait(false);
+
+ var stream = new NetworkStream(clientSocket, ownsSocket: true);
+ var session = new RpcConnection(stream, this.registry);
+ this.sessions.TryAdd(session.Id, session);
+
+ this.log.Debug("RPC connection created: {Id}", session.Id);
+
+ _ = session.Completion.ContinueWith(t =>
+ {
+ this.sessions.TryRemove(session.Id, out _);
+ this.log.Debug("RPC connection removed: {Id}", session.Id);
+ }, TaskScheduler.Default);
+ }
+ catch (OperationCanceledException)
+ {
+ clientSocket?.Dispose();
+ break;
+ }
+ catch (Exception ex)
+ {
+ clientSocket?.Dispose();
+ this.log.Error(ex, "Error in socket accept loop");
+ await Task.Delay(500, token).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ this.log.Error(ex, "Fatal error in Unix socket accept loop");
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
index 5d2d32728..bff5c8ba2 100644
--- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs
+++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
-using Dalamud.Networking.Pipes;
+using Dalamud.Networking.Rpc.Model;
namespace Dalamud.Plugin.Services;
diff --git a/Dalamud/Utility/UnixSocketUtil.cs b/Dalamud/Utility/UnixSocketUtil.cs
new file mode 100644
index 000000000..46bb05c74
--- /dev/null
+++ b/Dalamud/Utility/UnixSocketUtil.cs
@@ -0,0 +1,92 @@
+using System.IO;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+using Serilog;
+
+namespace Dalamud.Utility;
+
+///
+/// A set of utilities to help manage Unix sockets.
+///
+internal static class UnixSocketUtil
+{
+ // Default probe timeout in milliseconds.
+ private const int DefaultProbeMs = 200;
+
+ ///
+ /// Test whether a Unix socket is alive/listening.
+ ///
+ /// The path to test.
+ /// How long to wait for a connection success.
+ /// A task result representing if a socket is alive or not.
+ public static async Task IsSocketAlive(string path, int timeoutMs = DefaultProbeMs)
+ {
+ if (string.IsNullOrEmpty(path)) return false;
+ var endpoint = new UnixDomainSocketEndPoint(path);
+ using var client = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
+
+ var connectTask = client.ConnectAsync(endpoint);
+ var completed = await Task.WhenAny(connectTask, Task.Delay(timeoutMs)).ConfigureAwait(false);
+
+ if (completed == connectTask)
+ {
+ // Connected or failed very quickly. If the task is successful, the socket is alive.
+ if (connectTask.IsCompletedSuccessfully)
+ {
+ try
+ {
+ client.Shutdown(SocketShutdown.Both);
+ }
+ catch
+ {
+ // ignored
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Find and remove stale Dalamud RPC sockets.
+ ///
+ /// The directory to scan for stale sockets.
+ /// The timeout to wait for a connection attempt to succeed.
+ /// A task that executes when sockets are purged.
+ public static async Task CleanStaleSockets(string directory, int probeTimeoutMs = DefaultProbeMs)
+ {
+ if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return;
+
+ foreach (var file in Directory.EnumerateFiles(directory, "DalamudRPC.*.sock", SearchOption.TopDirectoryOnly))
+ {
+ // we don't need to check ourselves.
+ if (file.Contains(Environment.ProcessId.ToString())) continue;
+
+ bool shouldDelete;
+
+ try
+ {
+ shouldDelete = !await IsSocketAlive(file, probeTimeoutMs);
+ }
+ catch
+ {
+ shouldDelete = true;
+ }
+
+ if (shouldDelete)
+ {
+ try
+ {
+ File.Delete(file);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Could not delete stale socket file: {File}", file);
+ }
+ }
+ }
+ }
+}
From 01d8fc0c7ea177bdbe46ec5ebf8c9cd910556852 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Tue, 18 Nov 2025 15:57:37 -0800
Subject: [PATCH 032/201] fix: log tweaks
- also fix a boot failure
---
Dalamud/Networking/Rpc/RpcHostService.cs | 4 ++--
Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs | 3 +--
Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs | 3 +--
Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs | 4 +++-
Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +-
5 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs
index f164992eb..60152b355 100644
--- a/Dalamud/Networking/Rpc/RpcHostService.cs
+++ b/Dalamud/Networking/Rpc/RpcHostService.cs
@@ -88,7 +88,7 @@ internal class RpcHostService : IServiceType, IInternalDisposableService
var transport = new UnixRpcTransport(this.registry);
this.transports.Add(transport);
transport.Start();
- this.log.Information("RpcHostService started Unix socket host: {Socket}", transport.SocketPath);
+ this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath);
}
private void StartPipeTransport()
@@ -100,6 +100,6 @@ internal class RpcHostService : IServiceType, IInternalDisposableService
var transport = new PipeRpcTransport(this.registry);
this.transports.Add(transport);
transport.Start();
- this.log.Information("RpcHostService started named pipe host: {Pipe}", transport.PipeName);
+ this.log.Information("RpcHostService listening to named pipe: {Pipe}", transport.PipeName);
}
}
diff --git a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs
index 0cefeb853..727eb9125 100644
--- a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs
+++ b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs
@@ -16,7 +16,7 @@ namespace Dalamud.Networking.Rpc.Transport;
///
internal class PipeRpcTransport : IRpcTransport
{
- private readonly ModuleLog log = new("RPC/Host");
+ private readonly ModuleLog log = new("RPC/Transport/NamedPipe");
private readonly RpcServiceRegistry registry;
private readonly CancellationTokenSource cts = new();
@@ -107,7 +107,6 @@ internal class PipeRpcTransport : IRpcTransport
private async Task AcceptLoopAsync()
{
- this.log.Information("PipeRpcHost starting on pipe {Pipe}", this.PipeName);
var token = this.cts.Token;
var security = this.BuildPipeSecurity();
diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
index 3019f5aaf..e1ef64f76 100644
--- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
+++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
@@ -17,7 +17,7 @@ namespace Dalamud.Networking.Rpc.Transport;
///
internal class UnixRpcTransport : IRpcTransport
{
- private readonly ModuleLog log = new("RPC/UnixHost");
+ private readonly ModuleLog log = new("RPC/Transport/UnixSocket");
private readonly RpcServiceRegistry registry;
private readonly CancellationTokenSource cts = new();
@@ -173,7 +173,6 @@ internal class UnixRpcTransport : IRpcTransport
private async Task AcceptLoopAsync()
{
- this.log.Information("UnixRpcHost starting on socket {Socket}", this.SocketPath);
var token = this.cts.Token;
try
diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs
index af3b583c9..7e9faf3f9 100644
--- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs
+++ b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs
@@ -1,5 +1,7 @@
using System.Collections.Generic;
+using Dalamud.Plugin.Services;
+
namespace Dalamud.Plugin.SelfTest;
///
@@ -44,7 +46,7 @@ namespace Dalamud.Plugin.SelfTest;
/// }
///
///
-public interface ISelfTestRegistry
+public interface ISelfTestRegistry : IDalamudService
{
///
/// Registers the self-test steps for this plugin.
diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
index bff5c8ba2..37101222a 100644
--- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs
+++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
@@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services;
/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace.
///
[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")]
-public interface IPluginLinkHandler
+public interface IPluginLinkHandler : IDalamudService
{
///
/// A delegate containing the received URI.
From 0d8f577576800d9e00bdf262562f4ba4c3e910ad Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Tue, 18 Nov 2025 16:28:03 -0800
Subject: [PATCH 033/201] feat: add debug link handler as demo
---
.../Rpc/Service/Links/DebugLinkHandler.cs | 67 +++++++++++++++++++
.../Links}/PluginLinkHandler.cs | 3 +-
2 files changed, 68 insertions(+), 2 deletions(-)
create mode 100644 Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs
rename Dalamud/Networking/Rpc/{Api => Service/Links}/PluginLinkHandler.cs (95%)
diff --git a/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs
new file mode 100644
index 000000000..269617fc0
--- /dev/null
+++ b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs
@@ -0,0 +1,67 @@
+using Dalamud.Game.Gui.Toast;
+using Dalamud.Interface.ImGuiNotification;
+using Dalamud.Interface.ImGuiNotification.Internal;
+using Dalamud.Networking.Rpc.Model;
+
+namespace Dalamud.Networking.Rpc.Service.Links;
+
+#if DEBUG
+
+///
+/// A debug controller for link handling.
+///
+[ServiceManager.EarlyLoadedService]
+internal sealed class DebugLinkHandler : IInternalDisposableService
+{
+ private readonly LinkHandlerService linkHandlerService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Injected LinkHandler.
+ [ServiceManager.ServiceConstructor]
+ public DebugLinkHandler(LinkHandlerService linkHandler)
+ {
+ this.linkHandlerService = linkHandler;
+
+ this.linkHandlerService.Register("debug", this.HandleLink);
+ }
+
+ ///
+ public void DisposeService()
+ {
+ this.linkHandlerService.Unregister("debug", this.HandleLink);
+ }
+
+ private void HandleLink(DalamudUri uri)
+ {
+ var action = uri.Path.Split("/").GetValue(1)?.ToString();
+ switch (action)
+ {
+ case "toast":
+ this.ShowToast(uri);
+ break;
+ case "notification":
+ this.ShowNotification(uri);
+ break;
+ }
+ }
+
+ private void ShowToast(DalamudUri uri)
+ {
+ var message = uri.QueryParams.Get("message") ?? "Hello, world!";
+ Service.Get().ShowNormal(message);
+ }
+
+ private void ShowNotification(DalamudUri uri)
+ {
+ Service.Get().AddNotification(
+ new Notification
+ {
+ Title = uri.QueryParams.Get("title"),
+ Content = uri.QueryParams.Get("content") ?? "Hello, world!",
+ });
+ }
+}
+
+#endif
diff --git a/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs
similarity index 95%
rename from Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs
rename to Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs
index e9372bf0e..4dbe3fdf1 100644
--- a/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs
+++ b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs
@@ -4,13 +4,12 @@ using Dalamud.Console;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Networking.Rpc.Model;
-using Dalamud.Networking.Rpc.Service;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
#pragma warning disable DAL_RPC
-namespace Dalamud.Networking.Rpc.Api;
+namespace Dalamud.Networking.Rpc.Service.Links;
///
[PluginInterface]
From 7b286c427cbd14859381ce7ee99bef1d9768e033 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Tue, 25 Nov 2025 10:08:24 -0800
Subject: [PATCH 034/201] chore: remove named pipe transport, use startinfo for
pathing
---
Dalamud.Boot/DalamudStartInfo.cpp | 5 +
Dalamud.Boot/DalamudStartInfo.h | 1 +
Dalamud.Boot/veh.cpp | 15 +-
Dalamud.Common/DalamudStartInfo.cs | 6 +
Dalamud.Injector/Program.cs | 6 +
Dalamud/Networking/Rpc/RpcHostService.cs | 14 --
.../Rpc/Transport/PipeRpcTransport.cs | 154 ------------------
.../Rpc/Transport/UnixRpcTransport.cs | 34 ++--
.../Rpc}/UnixSocketUtil.cs | 2 +-
9 files changed, 41 insertions(+), 196 deletions(-)
delete mode 100644 Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs
rename Dalamud/{Utility => Networking/Rpc}/UnixSocketUtil.cs (98%)
diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp
index 5be8f97d0..9c8fd9721 100644
--- a/Dalamud.Boot/DalamudStartInfo.cpp
+++ b/Dalamud.Boot/DalamudStartInfo.cpp
@@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.LogName = json.value("LogName", config.LogName);
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
+
+ if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) {
+ config.TempDirectory = json.value("TempDirectory", config.TempDirectory);
+ }
+
config.Language = json.value("Language", config.Language);
config.Platform = json.value("Platform", config.Platform);
config.GameVersion = json.value("GameVersion", config.GameVersion);
diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h
index 0eeaddeed..308dcab7d 100644
--- a/Dalamud.Boot/DalamudStartInfo.h
+++ b/Dalamud.Boot/DalamudStartInfo.h
@@ -44,6 +44,7 @@ struct DalamudStartInfo {
std::string ConfigurationPath;
std::string LogPath;
std::string LogName;
+ std::string TempDirectory;
std::string PluginDirectory;
std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English;
diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp
index b0ec1cefa..b75256af8 100644
--- a/Dalamud.Boot/veh.cpp
+++ b/Dalamud.Boot/veh.cpp
@@ -122,6 +122,7 @@ static DalamudExpected append_injector_launch_args(std::vector(g_startInfo.LogName) + L"\"");
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert(g_startInfo.PluginDirectory) + L"\"");
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert(g_startInfo.AssetDirectory) + L"\"");
+ args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert(g_startInfo.TempDirectory) + L"\"");
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast(g_startInfo.Language)));
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
@@ -268,7 +269,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) &&
!is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip))
- return EXCEPTION_CONTINUE_SEARCH;
+ return EXCEPTION_CONTINUE_SEARCH;
}
return exception_handler(ex);
@@ -297,7 +298,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536))
{
hWritePipe.emplace(hWritePipeRaw, &CloseHandle);
-
+
if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE))
{
hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle);
@@ -315,9 +316,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
}
// additional information
- STARTUPINFOEXW siex{};
+ STARTUPINFOEXW siex{};
PROCESS_INFORMATION pi{};
-
+
siex.StartupInfo.cb = sizeof siex;
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE;
@@ -385,7 +386,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
argstr.push_back(L' ');
}
argstr.pop_back();
-
+
if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr))
{
logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError());
@@ -400,7 +401,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
TRUE, // Set handle inheritance to FALSE
EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W)
nullptr, // Use parent's environment block
- nullptr, // Use parent's starting directory
+ nullptr, // Use parent's starting directory
&siex.StartupInfo, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
))
@@ -416,7 +417,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
}
CloseHandle(pi.hThread);
-
+
g_crashhandler_process = pi.hProcess;
g_crashhandler_pipe_write = hWritePipe->release();
logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId);
diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs
index a0d7f8b0b..8c66a85ba 100644
--- a/Dalamud.Common/DalamudStartInfo.cs
+++ b/Dalamud.Common/DalamudStartInfo.cs
@@ -34,6 +34,12 @@ public record DalamudStartInfo
///
public string? ConfigurationPath { get; set; }
+ ///
+ /// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user.
+ /// It should also be predictable and easy for launchers to find.
+ ///
+ public string? TempDirectory { get; set; }
+
///
/// Gets or sets the path of the log files.
///
diff --git a/Dalamud.Injector/Program.cs b/Dalamud.Injector/Program.cs
index e224791e6..13fcacef2 100644
--- a/Dalamud.Injector/Program.cs
+++ b/Dalamud.Injector/Program.cs
@@ -291,6 +291,7 @@ namespace Dalamud.Injector
var configurationPath = startInfo.ConfigurationPath;
var pluginDirectory = startInfo.PluginDirectory;
var assetDirectory = startInfo.AssetDirectory;
+ var tempDirectory = startInfo.TempDirectory;
var delayInitializeMs = startInfo.DelayInitializeMs;
var logName = startInfo.LogName;
var logPath = startInfo.LogPath;
@@ -321,6 +322,10 @@ namespace Dalamud.Injector
{
assetDirectory = args[i][key.Length..];
}
+ else if (args[i].StartsWith(key = "--dalamud-temp-directory="))
+ {
+ tempDirectory = args[i][key.Length..];
+ }
else if (args[i].StartsWith(key = "--dalamud-delay-initialize="))
{
delayInitializeMs = int.Parse(args[i][key.Length..]);
@@ -433,6 +438,7 @@ namespace Dalamud.Injector
startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory;
startInfo.AssetDirectory = assetDirectory;
+ startInfo.TempDirectory = tempDirectory;
startInfo.Language = clientLanguage;
startInfo.Platform = platform;
startInfo.DelayInitializeMs = delayInitializeMs;
diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs
index 60152b355..bbe9dc8eb 100644
--- a/Dalamud/Networking/Rpc/RpcHostService.cs
+++ b/Dalamud/Networking/Rpc/RpcHostService.cs
@@ -5,7 +5,6 @@ using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Networking.Rpc.Transport;
-using Dalamud.Utility;
namespace Dalamud.Networking.Rpc;
@@ -26,7 +25,6 @@ internal class RpcHostService : IServiceType, IInternalDisposableService
public RpcHostService()
{
this.StartUnixTransport();
- this.StartPipeTransport();
if (this.transports.Count == 0)
{
@@ -90,16 +88,4 @@ internal class RpcHostService : IServiceType, IInternalDisposableService
transport.Start();
this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath);
}
-
- private void StartPipeTransport()
- {
- // Wine doesn't support named pipes.
- if (Util.IsWine())
- return;
-
- var transport = new PipeRpcTransport(this.registry);
- this.transports.Add(transport);
- transport.Start();
- this.log.Information("RpcHostService listening to named pipe: {Pipe}", transport.PipeName);
- }
}
diff --git a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs
deleted file mode 100644
index 727eb9125..000000000
--- a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.IO.Pipes;
-using System.Security.AccessControl;
-using System.Security.Principal;
-using System.Threading;
-using System.Threading.Tasks;
-
-using Dalamud.Logging.Internal;
-using Dalamud.Utility;
-
-namespace Dalamud.Networking.Rpc.Transport;
-
-///
-/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc.
-///
-internal class PipeRpcTransport : IRpcTransport
-{
- private readonly ModuleLog log = new("RPC/Transport/NamedPipe");
-
- private readonly RpcServiceRegistry registry;
- private readonly CancellationTokenSource cts = new();
- private readonly ConcurrentDictionary sessions = new();
- private Task? acceptLoopTask;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The RPC service registry to use.
- /// The pipe name to create.
- public PipeRpcTransport(RpcServiceRegistry registry, string? pipeName = null)
- {
- this.registry = registry;
- // Default pipe name based on current process ID for uniqueness per Dalamud instance.
- this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}";
- }
-
- ///
- /// Gets the name of the named pipe this RPC host is using.
- ///
- public string PipeName { get; }
-
- ///
- public IReadOnlyDictionary Connections => this.sessions;
-
- /// Starts accepting client connections.
- public void Start()
- {
- if (this.acceptLoopTask != null) return;
- this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning);
- }
-
- /// Invoke an RPC request on a specific client expecting a result.
- /// The client ID to invoke.
- /// The method to invoke.
- /// Any arguments to invoke.
- /// An optional return based on the specified RPC.
- /// The expected response type.
- public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments)
- {
- if (!this.sessions.TryGetValue(clientId, out var session))
- throw new KeyNotFoundException($"No client {clientId}");
-
- return session.Rpc.InvokeAsync(method, arguments);
- }
-
- /// Send a notification to all connected clients (no response expected).
- /// The method name to broadcast.
- /// The arguments to broadcast.
- /// Returns a Task when completed.
- public Task BroadcastNotifyAsync(string method, params object[] arguments)
- {
- var list = this.sessions.Values;
- var tasks = new List(list.Count);
- foreach (var s in list)
- {
- tasks.Add(s.Rpc.NotifyAsync(method, arguments));
- }
-
- return Task.WhenAll(tasks);
- }
-
- ///
- public void Dispose()
- {
- this.cts.Cancel();
- this.acceptLoopTask?.Wait(1000);
-
- foreach (var kv in this.sessions)
- {
- kv.Value.Dispose();
- }
-
- this.sessions.Clear();
- this.cts.Dispose();
- this.log.Information("PipeRpcHost disposed ({Pipe})", this.PipeName);
- GC.SuppressFinalize(this);
- }
-
- private PipeSecurity BuildPipeSecurity()
- {
- var ps = new PipeSecurity();
- ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User!, PipeAccessRights.FullControl, AccessControlType.Allow));
-
- return ps;
- }
-
- private async Task AcceptLoopAsync()
- {
- var token = this.cts.Token;
- var security = this.BuildPipeSecurity();
-
- while (!token.IsCancellationRequested)
- {
- NamedPipeServerStream? server = null;
- try
- {
- server = NamedPipeServerStreamAcl.Create(
- this.PipeName,
- PipeDirection.InOut,
- NamedPipeServerStream.MaxAllowedServerInstances,
- PipeTransmissionMode.Message,
- PipeOptions.Asynchronous,
- 65536,
- 65536,
- security);
-
- await server.WaitForConnectionAsync(token).ConfigureAwait(false);
-
- var session = new RpcConnection(server, this.registry);
- this.sessions.TryAdd(session.Id, session);
-
- this.log.Debug("RPC connection created: {Id}", session.Id);
-
- _ = session.Completion.ContinueWith(t =>
- {
- this.sessions.TryRemove(session.Id, out _);
- this.log.Debug("RPC connection removed: {Id}", session.Id);
- }, TaskScheduler.Default);
- }
- catch (OperationCanceledException)
- {
- server?.Dispose();
- break;
- }
- catch (Exception ex)
- {
- server?.Dispose();
- this.log.Error(ex, "Error in pipe accept loop");
- await Task.Delay(500, token).ConfigureAwait(false);
- }
- }
- }
-}
diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
index e1ef64f76..064ce375d 100644
--- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
+++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
@@ -31,30 +31,30 @@ internal class UnixRpcTransport : IRpcTransport
/// Initializes a new instance of the class.
///
/// The RPC service registry to use.
- /// The Unix socket path to create. If null, defaults to a path based on process ID.
- public UnixRpcTransport(RpcServiceRegistry registry, string? socketPath = null)
+ /// The Unix socket directory to use. If null, defaults to Dalamud home directory.
+ /// The name of the socket to create.
+ public UnixRpcTransport(RpcServiceRegistry registry, string? socketDirectory = null, string? socketName = null)
{
this.registry = registry;
+ socketName ??= $"DalamudRPC.{Environment.ProcessId}.sock";
- if (socketPath != null)
+ if (!socketDirectory.IsNullOrEmpty())
{
- this.SocketPath = socketPath;
+ this.SocketPath = Path.Combine(socketDirectory, socketName);
}
else
{
- var dalamudConfigPath = Service.Get().StartInfo.ConfigurationPath;
- var dalamudHome = Path.GetDirectoryName(dalamudConfigPath);
- var socketName = $"DalamudRPC.{Environment.ProcessId}.sock";
+ socketDirectory = Service.Get().StartInfo.TempDirectory;
- if (dalamudHome == null)
+ if (socketDirectory == null)
{
this.SocketPath = Path.Combine(Path.GetTempPath(), socketName);
- this.log.Warning("Dalamud home is empty! UDS socket will be in temp.");
+ this.log.Warning("Temp dir was not set in StartInfo; using system temp for unix socket.");
}
else
{
- this.SocketPath = Path.Combine(dalamudHome, socketName);
- this.cleanupSocketDirectory = dalamudHome;
+ this.SocketPath = Path.Combine(socketDirectory, socketName);
+ this.cleanupSocketDirectory = socketDirectory;
}
}
}
@@ -76,15 +76,8 @@ internal class UnixRpcTransport : IRpcTransport
var socketDir = Path.GetDirectoryName(this.SocketPath);
if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir))
{
- try
- {
- Directory.CreateDirectory(socketDir);
- }
- catch (Exception ex)
- {
- this.log.Error(ex, "Failed to create socket directory: {Path}", socketDir);
- return;
- }
+ this.log.Error("Directory for unix socket does not exist: {Path}", socketDir);
+ return;
}
// Delete existing socket for this PID, if it exists.
@@ -103,6 +96,7 @@ internal class UnixRpcTransport : IRpcTransport
this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning);
// note: needs to be run _after_ we're alive so that we don't delete our own socket.
+ // TODO: This should *probably* be handed by the launcher instead.
if (this.cleanupSocketDirectory != null)
{
Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory));
diff --git a/Dalamud/Utility/UnixSocketUtil.cs b/Dalamud/Networking/Rpc/UnixSocketUtil.cs
similarity index 98%
rename from Dalamud/Utility/UnixSocketUtil.cs
rename to Dalamud/Networking/Rpc/UnixSocketUtil.cs
index 46bb05c74..b7500a946 100644
--- a/Dalamud/Utility/UnixSocketUtil.cs
+++ b/Dalamud/Networking/Rpc/UnixSocketUtil.cs
@@ -4,7 +4,7 @@ using System.Threading.Tasks;
using Serilog;
-namespace Dalamud.Utility;
+namespace Dalamud.Networking.Rpc;
///
/// A set of utilities to help manage Unix sockets.
From 8ab7b59ae47f5deaa59dc1001e92855c3b6e2a89 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Tue, 25 Nov 2025 10:17:12 -0800
Subject: [PATCH 035/201] fix: Missing service types causing injection failures
---
Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs | 4 +++-
Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs
index af3b583c9..7e9faf3f9 100644
--- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs
+++ b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs
@@ -1,5 +1,7 @@
using System.Collections.Generic;
+using Dalamud.Plugin.Services;
+
namespace Dalamud.Plugin.SelfTest;
///
@@ -44,7 +46,7 @@ namespace Dalamud.Plugin.SelfTest;
/// }
///
///
-public interface ISelfTestRegistry
+public interface ISelfTestRegistry : IDalamudService
{
///
/// Registers the self-test steps for this plugin.
diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
index 5d2d32728..c05757ac7 100644
--- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs
+++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs
@@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services;
/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace.
///
[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")]
-public interface IPluginLinkHandler
+public interface IPluginLinkHandler : IDalamudService
{
///
/// A delegate containing the received URI.
From 9a1fae8246d0d55cf9d52b0c31c28956b456c6d8 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Tue, 25 Nov 2025 17:27:48 -0800
Subject: [PATCH 036/201] Refactor Addon Lifecycle
---
.../Game/Addon/AddonLifecyclePooledArgs.cs | 107 -----
.../Lifecycle/AddonArgTypes/AddonArgs.cs | 2 +-
.../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +-
.../AddonArgTypes/AddonFinalizeArgs.cs | 8 +-
.../AddonArgTypes/AddonGenericArgs.cs | 18 +
.../AddonArgTypes/AddonReceiveEventArgs.cs | 16 +-
.../AddonArgTypes/AddonRefreshArgs.cs | 12 +-
.../AddonArgTypes/AddonRequestedUpdateArgs.cs | 12 +-
.../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 12 +-
.../AddonArgTypes/AddonUpdateArgs.cs | 10 +-
Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +-
Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 54 ++-
.../Game/Addon/Lifecycle/AddonLifecycle.cs | 298 ++-----------
.../AddonLifecycleAddressResolver.cs | 38 +-
.../AddonLifecycleReceiveEventListener.cs | 112 -----
.../Game/Addon/Lifecycle/AddonSetupHook.cs | 80 ----
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 405 ++++++++++++++++++
Dalamud/Hooking/Internal/CallHook.cs | 100 -----
.../Data/Widgets/AddonLifecycleWidget.cs | 51 ---
19 files changed, 543 insertions(+), 825 deletions(-)
delete mode 100644 Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs
create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
delete mode 100644 Dalamud/Hooking/Internal/CallHook.cs
diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
deleted file mode 100644
index 14def2036..000000000
--- a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Threading;
-
-using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-
-namespace Dalamud.Game.Addon;
-
-/// Argument pool for Addon Lifecycle services.
-[ServiceManager.EarlyLoadedService]
-internal sealed class AddonLifecyclePooledArgs : IServiceType
-{
- private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
- private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
- private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
- private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
- private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
- private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
- private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
-
- [ServiceManager.ServiceConstructor]
- private AddonLifecyclePooledArgs()
- {
- }
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonRequestedUpdateArgs arg) =>
- new(out arg, this.addonRequestedUpdateArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonReceiveEventArgs arg) =>
- new(out arg, this.addonReceiveEventArgPool);
-
- /// Returns the object to the pool on dispose.
- /// The type.
- public readonly ref struct PooledEntry
- where T : AddonArgs, new()
- {
- private readonly Span pool;
- private readonly T obj;
-
- /// Initializes a new instance of the struct.
- /// An instance of the argument.
- /// The pool to rent from and return to.
- public PooledEntry(out T arg, Span pool)
- {
- this.pool = pool;
- foreach (ref var item in pool)
- {
- if (Interlocked.Exchange(ref item, null) is { } v)
- {
- this.obj = arg = v;
- return;
- }
- }
-
- this.obj = arg = new();
- }
-
- /// Returns the item to the pool.
- public void Dispose()
- {
- var tmp = this.obj;
- foreach (ref var item in this.pool)
- {
- if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
- return;
- tmp = tmp2;
- }
- }
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
index c008db08f..0b2ae1178 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Base class for AddonLifecycle AddonArgTypes.
///
-public abstract unsafe class AddonArgs
+public abstract class AddonArgs
{
///
/// Constant string representing the name of an addon that is invalid.
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
index 989e11912..7254ba7b3 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
@@ -3,7 +3,7 @@
///
/// Addon argument data for Draw events.
///
-public class AddonDrawArgs : AddonArgs, ICloneable
+public class AddonDrawArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -15,10 +15,4 @@ public class AddonDrawArgs : AddonArgs, ICloneable
///
public override AddonArgsType Type => AddonArgsType.Draw;
-
- ///
- public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
index d9401b414..12def3ad3 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for ReceiveEvent events.
///
-public class AddonFinalizeArgs : AddonArgs, ICloneable
+public class AddonFinalizeArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -15,10 +15,4 @@ public class AddonFinalizeArgs : AddonArgs, ICloneable
///
public override AddonArgsType Type => AddonArgsType.Finalize;
-
- ///
- public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
new file mode 100644
index 000000000..f3078af69
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
@@ -0,0 +1,18 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Draw events.
+///
+public class AddonGenericArgs : AddonArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonGenericArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Generic;
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
index 980fe4f2f..05f51b118 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for ReceiveEvent events.
///
-public class AddonReceiveEventArgs : AddonArgs, ICloneable
+public class AddonReceiveEventArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -36,19 +36,13 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
///
public nint Data { get; set; }
- ///
- public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.AtkEventType = default;
- this.EventParam = default;
- this.AtkEvent = default;
- this.Data = default;
+ this.AtkEventType = 0;
+ this.EventParam = 0;
+ this.AtkEvent = 0;
+ this.Data = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
index d28631c3c..c01c065c1 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Refresh events.
///
-public class AddonRefreshArgs : AddonArgs, ICloneable
+public class AddonRefreshArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -33,17 +33,11 @@ public class AddonRefreshArgs : AddonArgs, ICloneable
///
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
- ///
- public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.AtkValueCount = default;
- this.AtkValues = default;
+ this.AtkValueCount = 0;
+ this.AtkValues = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
index e87a980fd..bf00c5d6e 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for OnRequestedUpdate events.
///
-public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
+public class AddonRequestedUpdateArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -26,17 +26,11 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
///
public nint StringArrayData { get; set; }
- ///
- public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.NumberArrayData = default;
- this.StringArrayData = default;
+ this.NumberArrayData = 0;
+ this.StringArrayData = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
index 0dd9ecee2..9b7e86a61 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Setup events.
///
-public class AddonSetupArgs : AddonArgs, ICloneable
+public class AddonSetupArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -33,17 +33,11 @@ public class AddonSetupArgs : AddonArgs, ICloneable
///
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
- ///
- public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.AtkValueCount = default;
- this.AtkValues = default;
+ this.AtkValueCount = 0;
+ this.AtkValues = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
index a263f6ae4..bab62fc89 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Update events.
///
-public class AddonUpdateArgs : AddonArgs, ICloneable
+public class AddonUpdateArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -30,16 +30,10 @@ public class AddonUpdateArgs : AddonArgs, ICloneable
///
internal float TimeDeltaInternal { get; set; }
- ///
- public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.TimeDeltaInternal = default;
+ this.TimeDeltaInternal = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
index b58b5f4c7..95dc5f718 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
@@ -9,34 +9,39 @@ public enum AddonArgsType
/// Contains argument data for Setup.
///
Setup,
-
+
///
/// Contains argument data for Update.
///
Update,
-
+
///
/// Contains argument data for Draw.
- ///
+ ///
Draw,
-
+
///
/// Contains argument data for Finalize.
- ///
+ ///
Finalize,
-
+
///
/// Contains argument data for RequestedUpdate.
- ///
+ ///
RequestedUpdate,
-
+
///
/// Contains argument data for Refresh.
- ///
+ ///
Refresh,
-
+
///
/// Contains argument data for ReceiveEvent.
///
ReceiveEvent,
+
+ ///
+ /// Generic arg type that contains no meaningful data
+ ///
+ Generic,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
index 5fd0ac964..7738d6c6a 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
@@ -16,7 +16,7 @@ public enum AddonEvent
///
///
PreSetup,
-
+
///
/// An event that is fired after an addon has finished its initial setup. This event is particularly useful for
/// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data
@@ -64,7 +64,7 @@ public enum AddonEvent
///
///
PreFinalize,
-
+
///
/// An event that is fired before a call to is made in response to a
/// change in the subscribed or
@@ -81,13 +81,13 @@ public enum AddonEvent
/// to the Free Company's overview.
///
PreRequestedUpdate,
-
+
///
/// An event that is fired after an addon has finished processing an ArrayData update.
/// See for more information.
///
PostRequestedUpdate,
-
+
///
/// An event that is fired before an addon calls its method. Refreshes are
/// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to
@@ -96,13 +96,13 @@ public enum AddonEvent
///
///
PreRefresh,
-
+
///
/// An event that is fired after an addon has finished its refresh.
/// See for more information.
///
PostRefresh,
-
+
///
/// An event that is fired before an addon begins processing a user-driven event via
/// , such as mousing over an element or clicking a button. This event
@@ -112,10 +112,50 @@ public enum AddonEvent
///
///
PreReceiveEvent,
-
+
///
/// An event that is fired after an addon finishes calling its method.
/// See for more information.
///
PostReceiveEvent,
+
+ ///
+ /// An event that is fired before an addon processes its open method.
+ ///
+ PreOpen,
+
+ ///
+ /// An event that is fired after an addon has processed its open method.
+ ///
+ PostOpen,
+
+ ///
+ /// An even that is fired before an addon processes its close method.
+ ///
+ PreClose,
+
+ ///
+ /// An event that is fired after an addon has processed its close method.
+ ///
+ PostClose,
+
+ ///
+ /// An event that is fired before an addon processes its show method.
+ ///
+ PreShow,
+
+ ///
+ /// An event that is fired after an addon has processed its show method.
+ ///
+ PostShow,
+
+ ///
+ /// An event that is fired before an addon processes its hide method.
+ ///
+ PreHide,
+
+ ///
+ /// An event that is fired after an addon has processed its hide method.
+ ///
+ PostHide,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index b44ab8764..cea30d6be 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -1,16 +1,14 @@
using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics;
using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
-using Dalamud.Hooking.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
@@ -26,69 +24,33 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service.Get();
- [ServiceManager.ServiceDependency]
- private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
+ private readonly Dictionary modifiedTables = [];
- private readonly nint disallowedReceiveEventAddress;
-
- private readonly AddonLifecycleAddressResolver address;
- private readonly AddonSetupHook onAddonSetupHook;
- private readonly Hook onAddonFinalizeHook;
- private readonly CallHook onAddonDrawHook;
- private readonly CallHook onAddonUpdateHook;
- private readonly Hook onAddonRefreshHook;
- private readonly CallHook onAddonRequestedUpdateHook;
+ private Hook? onInitializeAddonHook;
[ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner)
{
- this.address = new AddonLifecycleAddressResolver();
- this.address.Setup(sigScanner);
+ this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
+ this.onInitializeAddonHook.Enable();
- this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent;
-
- var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon;
-
- this.onAddonSetupHook = new AddonSetupHook(this.address.AddonSetup, this.OnAddonSetup);
- this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
- this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw);
- this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate);
- this.onAddonRefreshHook = Hook.FromAddress(refreshAddonAddress, this.OnAddonRefresh);
- this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
-
- this.onAddonSetupHook.Enable();
- this.onAddonFinalizeHook.Enable();
- this.onAddonDrawHook.Enable();
- this.onAddonUpdateHook.Enable();
- this.onAddonRefreshHook.Enable();
- this.onAddonRequestedUpdateHook.Enable();
+ Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}");
}
- private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
-
- ///
- /// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
- ///
- internal List ReceiveEventListeners { get; } = new();
-
///
/// Gets a list of all AddonLifecycle Event Listeners.
///
- internal List EventListeners { get; } = new();
+ internal List EventListeners { get; } = [];
///
void IInternalDisposableService.DisposeService()
{
- this.onAddonSetupHook.Dispose();
- this.onAddonFinalizeHook.Dispose();
- this.onAddonDrawHook.Dispose();
- this.onAddonUpdateHook.Dispose();
- this.onAddonRefreshHook.Dispose();
- this.onAddonRequestedUpdateHook.Dispose();
+ this.onInitializeAddonHook?.Dispose();
+ this.onInitializeAddonHook = null;
- foreach (var receiveEventListener in this.ReceiveEventListeners)
+ foreach (var virtualTable in this.modifiedTables.Values)
{
- receiveEventListener.Dispose();
+ virtualTable.Dispose();
}
}
@@ -101,16 +63,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
this.framework.RunOnTick(() =>
{
this.EventListeners.Add(listener);
-
- // If we want receive event messages have an already active addon, enable the receive event hook.
- // If the addon isn't active yet, we'll grab the hook when it sets up.
- if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
- {
- if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
- {
- receiveEventListener.TryEnable();
- }
- }
});
}
@@ -122,24 +74,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{
// Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
listener.Removed = true;
-
+
this.framework.RunOnTick(() =>
{
this.EventListeners.Remove(listener);
-
- // If we are disabling an ReceiveEvent listener, check if we should disable the hook.
- if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
- {
- // Get the ReceiveEvent Listener for this addon
- if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
- {
- // If there are no other listeners listening for this event, disable the hook.
- if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
- {
- receiveEventListener.Disable();
- }
- }
- }
});
}
@@ -160,7 +98,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
// If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener.
if (listener.Removed)
continue;
-
+
// Match on string.empty for listeners that want events for all addons.
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
@@ -176,201 +114,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
}
- private void RegisterReceiveEventHook(AtkUnitBase* addon)
+ private void OnAddonInitialize(AtkUnitBase* addon)
{
- // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
- // Disallows hooking the core internal event handler.
- var addonName = addon->NameString;
- var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent;
- if (receiveEventAddress != this.disallowedReceiveEventAddress)
+ try
{
- // If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
- if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener)
+ this.LogInitialize(addon->NameString);
+
+ if (!this.modifiedTables.ContainsKey(addon->NameString))
{
- if (!existingListener.AddonNames.Contains(addonName))
+ // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
+ var managedVirtualTableEntry = new AddonVirtualTable(addon, this)
{
- existingListener.AddonNames.Add(addonName);
- }
- }
+ // This event is invoked when the game itself has disposed of an addon
+ // We can use this to know when to remove our virtual table entry
+ OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString),
+ };
- // Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
- else
- {
- this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
- }
-
- // If we have an active listener for this addon already, we need to activate this hook.
- if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
- {
- if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
- {
- receiveEventListener.TryEnable();
- }
+ this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry);
}
}
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
+ }
+
+ this.onInitializeAddonHook!.Original(addon);
}
- private void UnregisterReceiveEventHook(string addonName)
+ [Conditional("DEBUG")]
+ private void LogInitialize(string addonName)
{
- // Remove this addons ReceiveEvent Registration
- if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
- {
- eventListener.AddonNames.Remove(addonName);
-
- // If there are no more listeners let's remove and dispose.
- if (eventListener.AddonNames.Count is 0)
- {
- this.ReceiveEventListeners.Remove(eventListener);
- eventListener.Dispose();
- }
- }
- }
-
- private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
- {
- try
- {
- this.RegisterReceiveEventHook(addon);
- }
- catch (Exception e)
- {
- Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
- }
-
- using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.AtkValueCount = valueCount;
- arg.AtkValues = (nint)values;
- this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
- valueCount = arg.AtkValueCount;
- values = (AtkValue*)arg.AtkValues;
-
- try
- {
- addon->OnSetup(valueCount, values);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
- }
-
- private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
- {
- try
- {
- var addonName = atkUnitBase[0]->NameString;
- this.UnregisterReceiveEventHook(addonName);
- }
- catch (Exception e)
- {
- Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
- }
-
- using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
- arg.Clear();
- arg.Addon = (nint)atkUnitBase[0];
- this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
-
- try
- {
- this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
- }
- }
-
- private void OnAddonDraw(AtkUnitBase* addon)
- {
- using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
-
- try
- {
- addon->Draw();
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
- }
-
- private void OnAddonUpdate(AtkUnitBase* addon, float delta)
- {
- using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.TimeDeltaInternal = delta;
- this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
-
- try
- {
- addon->Update(delta);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
- }
-
- private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values)
- {
- var result = false;
-
- using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.AtkValueCount = valueCount;
- arg.AtkValues = (nint)values;
- this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
- valueCount = arg.AtkValueCount;
- values = (AtkValue*)arg.AtkValues;
-
- try
- {
- result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
- return result;
- }
-
- private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
- {
- using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.NumberArrayData = (nint)numberArrayData;
- arg.StringArrayData = (nint)stringArrayData;
- this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
- numberArrayData = (NumberArrayData**)arg.NumberArrayData;
- stringArrayData = (StringArrayData**)arg.StringArrayData;
-
- try
- {
- addon->OnRequestedUpdate(numberArrayData, stringArrayData);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
+ Log.Debug($"Initializing {addonName}");
}
}
@@ -387,7 +161,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service.Get();
- private readonly List eventListeners = new();
+ private readonly List eventListeners = [];
///
void IInternalDisposableService.DisposeService()
@@ -458,7 +232,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
this.eventListeners.RemoveAll(entry =>
{
if (entry.FunctionDelegate != handler) return false;
-
+
this.addonLifecycleService.UnregisterListener(entry);
return true;
});
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
index 854d666fd..1d767aac4 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
@@ -1,56 +1,24 @@
-using FFXIVClientStructs.FFXIV.Component.GUI;
+using Dalamud.Utility;
namespace Dalamud.Game.Addon.Lifecycle;
///
/// AddonLifecycleService memory address resolver.
///
-internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
+[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")]
+internal class AddonLifecycleAddressResolver : BaseAddressResolver
{
- ///
- /// Gets the address of the addon setup hook invoked by the AtkUnitManager.
- /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
- /// This is called for a majority of all addon OnSetup's.
- ///
- public nint AddonSetup { get; private set; }
-
- ///
- /// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
- /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
- /// This seems to be called rarely for specific addons.
- ///
- public nint AddonSetup2 { get; private set; }
-
///
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
///
public nint AddonFinalize { get; private set; }
- ///
- /// Gets the address of the addon draw hook invoked by virtual function call.
- ///
- public nint AddonDraw { get; private set; }
-
- ///
- /// Gets the address of the addon update hook invoked by virtual function call.
- ///
- public nint AddonUpdate { get; private set; }
-
- ///
- /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
- ///
- public nint AddonOnRequestedUpdate { get; private set; }
-
///
/// Scan for and setup any configured address pointers.
///
/// The signature scanner to facilitate setup.
protected override void Setup64Bit(ISigScanner sig)
{
- this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB");
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
- this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01");
- this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
- this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30");
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
deleted file mode 100644
index 0d2bcc7f2..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using System.Collections.Generic;
-
-using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-using Dalamud.Hooking;
-using Dalamud.Logging.Internal;
-
-using FFXIVClientStructs.FFXIV.Component.GUI;
-
-namespace Dalamud.Game.Addon.Lifecycle;
-
-///
-/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
-/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
-///
-internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
-{
- private static readonly ModuleLog Log = new("AddonLifecycle");
-
- [ServiceManager.ServiceDependency]
- private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// AddonLifecycle service instance.
- /// Initial Addon Requesting this listener.
- /// Address of Addon's ReceiveEvent function.
- internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
- {
- this.AddonLifecycle = service;
- this.AddonNames = [addonName];
- this.FunctionAddress = receiveEventAddress;
- }
-
- ///
- /// Gets the list of addons that use this receive event hook.
- ///
- public List AddonNames { get; init; }
-
- ///
- /// Gets the address of the ReceiveEvent function as provided by the vtable on setup.
- ///
- public nint FunctionAddress { get; init; }
-
- ///
- /// Gets the contained hook for these addons.
- ///
- public Hook? Hook { get; private set; }
-
- ///
- /// Gets or sets the Reference to AddonLifecycle service instance.
- ///
- private AddonLifecycle AddonLifecycle { get; set; }
-
- ///
- /// Try to hook and enable this receive event handler.
- ///
- public void TryEnable()
- {
- this.Hook ??= Hook.FromAddress(this.FunctionAddress, this.OnReceiveEvent);
- this.Hook?.Enable();
- }
-
- ///
- /// Disable the hook for this receive event handler.
- ///
- public void Disable()
- {
- this.Hook?.Disable();
- }
-
- ///
- public void Dispose()
- {
- this.Hook?.Dispose();
- }
-
- private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
- {
- // Check that we didn't get here through a call to another addons handler.
- var addonName = addon->NameString;
- if (!this.AddonNames.Contains(addonName))
- {
- this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
- return;
- }
-
- using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.AtkEventType = (byte)eventType;
- arg.EventParam = eventParam;
- arg.AtkEvent = (IntPtr)atkEvent;
- arg.Data = (nint)atkEventData;
- this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
- eventType = (AtkEventType)arg.AtkEventType;
- eventParam = arg.EventParam;
- atkEvent = (AtkEvent*)arg.AtkEvent;
- atkEventData = (AtkEventData*)arg.Data;
-
- try
- {
- this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs
deleted file mode 100644
index 297323b8f..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System.Runtime.InteropServices;
-
-using Reloaded.Hooks.Definitions;
-
-namespace Dalamud.Game.Addon.Lifecycle;
-
-///
-/// This class represents a callsite hook used to replace the address of the OnSetup function in r9.
-///
-/// Delegate signature for this hook.
-internal class AddonSetupHook : IDisposable where T : Delegate
-{
- private readonly Reloaded.Hooks.AsmHook asmHook;
-
- private T? detour;
- private bool activated;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Address of the instruction to replace.
- /// Delegate to invoke.
- internal AddonSetupHook(nint address, T detour)
- {
- this.detour = detour;
-
- var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
- var code = new[]
- {
- "use64",
- $"mov r9, 0x{detourPtr:X8}",
- };
-
- var opt = new AsmHookOptions
- {
- PreferRelativeJump = true,
- Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
- MaxOpcodeSize = 5,
- };
-
- this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
- }
-
- ///
- /// Gets a value indicating whether the hook is enabled.
- ///
- public bool IsEnabled => this.asmHook.IsEnabled;
-
- ///
- /// Starts intercepting a call to the function.
- ///
- public void Enable()
- {
- if (!this.activated)
- {
- this.activated = true;
- this.asmHook.Activate();
- return;
- }
-
- this.asmHook.Enable();
- }
-
- ///
- /// Stops intercepting a call to the function.
- ///
- public void Disable()
- {
- this.asmHook.Disable();
- }
-
- ///
- /// Remove a hook from the current process.
- ///
- public void Dispose()
- {
- this.asmHook.Disable();
- this.detour = null;
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
new file mode 100644
index 000000000..58e32a252
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -0,0 +1,405 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Logging.Internal;
+
+using FFXIVClientStructs.FFXIV.Client.System.Memory;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Lifecycle;
+
+///
+/// Represents a class that holds references to an addons original and modified virtual table entries.
+///
+internal unsafe class AddonVirtualTable : IDisposable
+{
+ // This need to be at minimum the largest virtual table size of all addons
+ // Copying extra entries is not problematic, and is considered safe.
+ private const int VirtualTableEntryCount = 200;
+
+ private const bool EnableAdvancedLogging = true;
+ private const bool EnableSpammyLogging = false;
+
+ private static readonly ModuleLog Log = new("LifecycleVT");
+
+ private readonly AddonLifecycle lifecycleService;
+
+ // Obsolete warning is only to prevent users from creating their own event objects.
+#pragma warning disable CS0618 // Type or member is obsolete
+ private readonly AddonSetupArgs addonSetupArg = new();
+ private readonly AddonFinalizeArgs addonFinalizeArg = new();
+ private readonly AddonDrawArgs addonDrawArg = new();
+ private readonly AddonUpdateArgs addonUpdateArg = new();
+ private readonly AddonRefreshArgs addonRefreshArg = new();
+ private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new();
+ private readonly AddonReceiveEventArgs addonReceiveEventArg = new();
+ private readonly AddonGenericArgs addonGenericArg = new();
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ private readonly AtkUnitBase* atkUnitBase;
+
+ private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable;
+ private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable;
+
+ // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
+ // the CLR needs to know they are in use, or it will invalidate them causing random crashing.
+ private readonly AtkUnitBase.Delegates.Dtor destructorFunction;
+ private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction;
+ private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction;
+ private readonly AtkUnitBase.Delegates.Draw drawFunction;
+ private readonly AtkUnitBase.Delegates.Update updateFunction;
+ private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction;
+ private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction;
+ private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction;
+ private readonly AtkUnitBase.Delegates.Open openFunction;
+ private readonly AtkUnitBase.Delegates.Close closeFunction;
+ private readonly AtkUnitBase.Delegates.Show showFunction;
+ private readonly AtkUnitBase.Delegates.Hide hideFunction;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// AtkUnitBase* for the addon to replace the table of.
+ /// Reference to AddonLifecycle service to callback and invoke listeners.
+ internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService)
+ {
+ this.atkUnitBase = addon;
+ this.lifecycleService = lifecycleService;
+
+ // Save original virtual table
+ this.originalVirtualTable = addon->VirtualTable;
+
+ // Create copy of original table
+ // Note this will copy any derived/overriden functions that this specific addon has.
+ // Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
+ this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
+ NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
+
+ // Overwrite the addons existing virtual table with our own
+ addon->VirtualTable = this.modifiedVirtualTable;
+
+ // Pin each of our listener functions
+ this.destructorFunction = this.OnAddonDestructor;
+ this.onSetupFunction = this.OnAddonSetup;
+ this.finalizerFunction = this.OnAddonFinalize;
+ this.drawFunction = this.OnAddonDraw;
+ this.updateFunction = this.OnAddonUpdate;
+ this.onRefreshFunction = this.OnAddonRefresh;
+ this.onRequestedUpdateFunction = this.OnRequestedUpdate;
+ this.onReceiveEventFunction = this.OnAddonReceiveEvent;
+ this.openFunction = this.OnAddonOpen;
+ this.closeFunction = this.OnAddonClose;
+ this.showFunction = this.OnAddonShow;
+ this.hideFunction = this.OnAddonHide;
+
+ // Overwrite specific virtual table entries
+ this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
+ this.modifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
+ this.modifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
+ this.modifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
+ this.modifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
+ this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
+ this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
+ this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
+ this.modifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction);
+ this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
+ this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction);
+ this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
+ }
+
+ ///
+ /// Gets an event that is invoked when this addon's Finalize method is called from native.
+ ///
+ public required Action OnAddonFinalized { get; init; }
+
+ ///
+ /// WARNING! This should not be called at any time except during dalamud unload.
+ ///
+ public void Dispose()
+ {
+ this.atkUnitBase->VirtualTable = this.originalVirtualTable;
+ IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
+ }
+
+ private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
+ {
+ this.LogEvent();
+
+ var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags);
+
+ if ((freeFlags & 1) == 1)
+ {
+ IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
+ this.OnAddonFinalized();
+ }
+
+ return result;
+ }
+
+ private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
+ {
+ this.LogEvent();
+
+ this.addonSetupArg.Clear();
+ this.addonSetupArg.Addon = addon;
+ this.addonSetupArg.AtkValueCount = valueCount;
+ this.addonSetupArg.AtkValues = (nint)values;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg);
+ valueCount = this.addonSetupArg.AtkValueCount;
+ values = (AtkValue*)this.addonSetupArg.AtkValues;
+
+ try
+ {
+ this.originalVirtualTable->OnSetup(addon, valueCount, values);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg);
+ }
+
+ private void OnAddonFinalize(AtkUnitBase* thisPtr)
+ {
+ this.LogEvent();
+
+ this.addonFinalizeArg.Clear();
+ this.addonFinalizeArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg);
+
+ try
+ {
+ this.originalVirtualTable->Finalizer(thisPtr);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
+ }
+ }
+
+ private void OnAddonDraw(AtkUnitBase* addon)
+ {
+ this.LogEvent();
+
+ this.addonDrawArg.Clear();
+ this.addonDrawArg.Addon = addon;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg);
+
+ try
+ {
+ this.originalVirtualTable->Draw(addon);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg);
+ }
+
+ private void OnAddonUpdate(AtkUnitBase* addon, float delta)
+ {
+ this.LogEvent();
+
+ this.addonUpdateArg.Clear();
+ this.addonUpdateArg.Addon = addon;
+ this.addonUpdateArg.TimeDeltaInternal = delta;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg);
+
+ try
+ {
+ this.originalVirtualTable->Update(addon, delta);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg);
+ }
+
+ private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
+ {
+ this.LogEvent();
+
+ var result = false;
+
+ this.addonRefreshArg.Clear();
+ this.addonRefreshArg.Addon = addon;
+ this.addonRefreshArg.AtkValueCount = valueCount;
+ this.addonRefreshArg.AtkValues = (nint)values;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg);
+ valueCount = this.addonRefreshArg.AtkValueCount;
+ values = (AtkValue*)this.addonRefreshArg.AtkValues;
+
+ try
+ {
+ result = this.originalVirtualTable->OnRefresh(addon, valueCount, values);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg);
+ return result;
+ }
+
+ private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
+ {
+ this.LogEvent();
+
+ this.addonRequestedUpdateArg.Clear();
+ this.addonRequestedUpdateArg.Addon = addon;
+ this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData;
+ this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg);
+ numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData;
+ stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData;
+
+ try
+ {
+ this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg);
+ }
+
+ private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
+ {
+ this.LogEvent();
+
+ this.addonReceiveEventArg.Clear();
+ this.addonReceiveEventArg.Addon = (nint)addon;
+ this.addonReceiveEventArg.AtkEventType = (byte)eventType;
+ this.addonReceiveEventArg.EventParam = eventParam;
+ this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent;
+ this.addonReceiveEventArg.Data = (nint)atkEventData;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg);
+ eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType;
+ eventParam = this.addonReceiveEventArg.EventParam;
+ atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent;
+ atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data;
+
+ try
+ {
+ this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg);
+ }
+
+ private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
+ {
+ this.LogEvent();
+
+ var result = false;
+
+ this.addonGenericArg.Clear();
+ this.addonGenericArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg);
+
+ try
+ {
+ result = this.originalVirtualTable->Open(thisPtr, depthLayer);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg);
+
+ return result;
+ }
+
+ private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
+ {
+ this.LogEvent();
+
+ var result = false;
+
+ this.addonGenericArg.Clear();
+ this.addonGenericArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg);
+
+ try
+ {
+ result = this.originalVirtualTable->Close(thisPtr, fireCallback);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg);
+
+ return result;
+ }
+
+ private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags)
+ {
+ this.LogEvent();
+
+ this.addonGenericArg.Clear();
+ this.addonGenericArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg);
+
+ try
+ {
+ this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg);
+ }
+
+ private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
+ {
+ this.LogEvent();
+
+ this.addonGenericArg.Clear();
+ this.addonGenericArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg);
+
+ try
+ {
+ this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg);
+ }
+
+ [Conditional("DEBUG")]
+ private void LogEvent([CallerMemberName] string caller = "")
+ {
+ if (EnableAdvancedLogging)
+ {
+ if (!EnableSpammyLogging)
+ {
+ if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
+ return;
+ }
+
+ Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}");
+ }
+ }
+}
diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs
deleted file mode 100644
index 92bc6e31a..000000000
--- a/Dalamud/Hooking/Internal/CallHook.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using System.Runtime.InteropServices;
-
-using Reloaded.Hooks.Definitions;
-
-namespace Dalamud.Hooking.Internal;
-
-///
-/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook.
-/// This is a destructive operation, no other callsite hooks can coexist at the same address.
-///
-/// There's no .Original for this hook type.
-/// This is only intended for be for functions where the parameters provided allow you to invoke the original call.
-///
-/// This class was specifically added for hooking virtual function callsites.
-/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered.
-///
-/// Delegate signature for this hook.
-internal class CallHook : IDalamudHook where T : Delegate
-{
- private readonly Reloaded.Hooks.AsmHook asmHook;
-
- private T? detour;
- private bool activated;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Address of the instruction to replace.
- /// Delegate to invoke.
- internal CallHook(nint address, T detour)
- {
- ArgumentNullException.ThrowIfNull(detour);
-
- this.detour = detour;
- this.Address = address;
-
- var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
- var code = new[]
- {
- "use64",
- $"mov rax, 0x{detourPtr:X8}",
- "call rax",
- };
-
- var opt = new AsmHookOptions
- {
- PreferRelativeJump = true,
- Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
- MaxOpcodeSize = 5,
- };
-
- this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
- }
-
- ///
- /// Gets a value indicating whether the hook is enabled.
- ///
- public bool IsEnabled => this.asmHook.IsEnabled;
-
- ///
- public IntPtr Address { get; }
-
- ///
- public string BackendName => "Reloaded AsmHook";
-
- ///
- public bool IsDisposed => this.detour == null;
-
- ///
- /// Starts intercepting a call to the function.
- ///
- public void Enable()
- {
- if (!this.activated)
- {
- this.activated = true;
- this.asmHook.Activate();
- return;
- }
-
- this.asmHook.Enable();
- }
-
- ///
- /// Stops intercepting a call to the function.
- ///
- public void Disable()
- {
- this.asmHook.Disable();
- }
-
- ///
- /// Remove a hook from the current process.
- ///
- public void Dispose()
- {
- this.asmHook.Disable();
- this.detour = null;
- }
-}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
index b58166e89..c336f895e 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -1,10 +1,8 @@
-using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle;
-using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@@ -54,13 +52,6 @@ public class AddonLifecycleWidget : IDataWindowWidget
this.DrawEventListeners();
ImGui.Unindent();
}
-
- if (ImGui.CollapsingHeader("ReceiveEvent Hooks"u8))
- {
- ImGui.Indent();
- this.DrawReceiveEventHooks();
- ImGui.Unindent();
- }
}
private void DrawEventListeners()
@@ -100,46 +91,4 @@ public class AddonLifecycleWidget : IDataWindowWidget
}
}
}
-
- private void DrawReceiveEventHooks()
- {
- if (!this.Ready) return;
-
- var listeners = this.AddonLifecycle.ReceiveEventListeners;
-
- if (listeners.Count == 0)
- {
- ImGui.Text("No ReceiveEvent Hooks are Registered"u8);
- }
-
- foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners)
- {
- if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames)))
- {
- ImGui.Columns(2);
-
- var functionAddress = receiveEventListener.FunctionAddress;
-
- ImGui.Text("Hook Address"u8);
- ImGui.NextColumn();
- ImGui.Text($"0x{functionAddress:X} (ffxiv_dx11.exe+{functionAddress - Process.GetCurrentProcess().MainModule!.BaseAddress:X})");
-
- ImGui.NextColumn();
- ImGui.Text("Hook Status"u8);
- ImGui.NextColumn();
- if (receiveEventListener.Hook is null)
- {
- ImGui.Text("Hook is null"u8);
- }
- else
- {
- var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
- var text = receiveEventListener.Hook.IsEnabled ? "Enabled"u8 : "Disabled"u8;
- ImGui.TextColored(color, text);
- }
-
- ImGui.Columns(1);
- }
- }
- }
}
From 2c1bb7664331d975c9398342b89cba88651df0b5 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Tue, 25 Nov 2025 18:56:34 -0800
Subject: [PATCH 037/201] Minor cleanup
---
Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 2 +-
Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 11 ++++++-----
.../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +-
Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +-
4 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
index 95dc5f718..de32bd254 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
@@ -41,7 +41,7 @@ public enum AddonArgsType
ReceiveEvent,
///
- /// Generic arg type that contains no meaningful data
+ /// Generic arg type that contains no meaningful data.
///
Generic,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index cea30d6be..0c23f5661 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -33,8 +33,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{
this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
this.onInitializeAddonHook.Enable();
-
- Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}");
}
///
@@ -48,10 +46,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
this.onInitializeAddonHook?.Dispose();
this.onInitializeAddonHook = null;
- foreach (var virtualTable in this.modifiedTables.Values)
+ this.framework.RunOnFrameworkThread(() =>
{
- virtualTable.Dispose();
- }
+ foreach (var virtualTable in this.modifiedTables.Values)
+ {
+ virtualTable.Dispose();
+ }
+ });
}
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
index 1d767aac4..9359870a5 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
///
/// AddonLifecycleService memory address resolver.
///
-[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")]
+[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")]
internal class AddonLifecycleAddressResolver : BaseAddressResolver
{
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index 58e32a252..ca5d970ef 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -19,7 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 200;
- private const bool EnableAdvancedLogging = true;
+ private const bool EnableAdvancedLogging = false;
private const bool EnableSpammyLogging = false;
private static readonly ModuleLog Log = new("LifecycleVT");
From ab0500ca6f9ff49cf0c48da7c585ae8e7429fbdf Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Tue, 25 Nov 2025 20:45:54 -0800
Subject: [PATCH 038/201] Fix unreachable code complaint
---
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 39 +++++++++----------
1 file changed, 18 insertions(+), 21 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index ca5d970ef..54c91248e 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -19,8 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 200;
- private const bool EnableAdvancedLogging = false;
- private const bool EnableSpammyLogging = false;
+ private const bool EnableLogging = false;
private static readonly ModuleLog Log = new("LifecycleVT");
@@ -125,7 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags);
@@ -140,7 +139,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonSetupArg.Clear();
this.addonSetupArg.Addon = addon;
@@ -164,7 +163,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonFinalize(AtkUnitBase* thisPtr)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonFinalizeArg.Clear();
this.addonFinalizeArg.Addon = thisPtr;
@@ -182,7 +181,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonDraw(AtkUnitBase* addon)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonDrawArg.Clear();
this.addonDrawArg.Addon = addon;
@@ -202,7 +201,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonUpdateArg.Clear();
this.addonUpdateArg.Addon = addon;
@@ -223,7 +222,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
var result = false;
@@ -250,7 +249,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonRequestedUpdateArg.Clear();
this.addonRequestedUpdateArg.Addon = addon;
@@ -274,7 +273,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonReceiveEventArg.Clear();
this.addonReceiveEventArg.Addon = (nint)addon;
@@ -302,7 +301,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
var result = false;
@@ -326,7 +325,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
var result = false;
@@ -350,7 +349,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
@@ -370,7 +369,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
@@ -389,15 +388,13 @@ internal unsafe class AddonVirtualTable : IDisposable
}
[Conditional("DEBUG")]
- private void LogEvent([CallerMemberName] string caller = "")
+ private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
{
- if (EnableAdvancedLogging)
+ if (loggingEnabled)
{
- if (!EnableSpammyLogging)
- {
- if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
- return;
- }
+ // Manually disable the really spammy log events, you can comment this out if you need to debug them.
+ if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
+ return;
Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}");
}
From 2cef75bbbef1862f85eb582c0b732c58c6b4a135 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Wed, 26 Nov 2025 11:56:30 -0800
Subject: [PATCH 039/201] feat: remove socket cleanup tasks
---
.../Rpc/Transport/UnixRpcTransport.cs | 9 --
Dalamud/Networking/Rpc/UnixSocketUtil.cs | 92 -------------------
2 files changed, 101 deletions(-)
delete mode 100644 Dalamud/Networking/Rpc/UnixSocketUtil.cs
diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
index 064ce375d..17da51444 100644
--- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
+++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
@@ -8,8 +8,6 @@ using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
-using TerraFX.Interop.Windows;
-
namespace Dalamud.Networking.Rpc.Transport;
///
@@ -94,13 +92,6 @@ internal class UnixRpcTransport : IRpcTransport
}
this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning);
-
- // note: needs to be run _after_ we're alive so that we don't delete our own socket.
- // TODO: This should *probably* be handed by the launcher instead.
- if (this.cleanupSocketDirectory != null)
- {
- Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory));
- }
}
/// Invoke an RPC request on a specific client expecting a result.
diff --git a/Dalamud/Networking/Rpc/UnixSocketUtil.cs b/Dalamud/Networking/Rpc/UnixSocketUtil.cs
deleted file mode 100644
index b7500a946..000000000
--- a/Dalamud/Networking/Rpc/UnixSocketUtil.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using System.IO;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-
-using Serilog;
-
-namespace Dalamud.Networking.Rpc;
-
-///
-/// A set of utilities to help manage Unix sockets.
-///
-internal static class UnixSocketUtil
-{
- // Default probe timeout in milliseconds.
- private const int DefaultProbeMs = 200;
-
- ///
- /// Test whether a Unix socket is alive/listening.
- ///
- /// The path to test.
- /// How long to wait for a connection success.
- /// A task result representing if a socket is alive or not.
- public static async Task IsSocketAlive(string path, int timeoutMs = DefaultProbeMs)
- {
- if (string.IsNullOrEmpty(path)) return false;
- var endpoint = new UnixDomainSocketEndPoint(path);
- using var client = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
-
- var connectTask = client.ConnectAsync(endpoint);
- var completed = await Task.WhenAny(connectTask, Task.Delay(timeoutMs)).ConfigureAwait(false);
-
- if (completed == connectTask)
- {
- // Connected or failed very quickly. If the task is successful, the socket is alive.
- if (connectTask.IsCompletedSuccessfully)
- {
- try
- {
- client.Shutdown(SocketShutdown.Both);
- }
- catch
- {
- // ignored
- }
-
- return true;
- }
- }
-
- return false;
- }
-
- ///
- /// Find and remove stale Dalamud RPC sockets.
- ///
- /// The directory to scan for stale sockets.
- /// The timeout to wait for a connection attempt to succeed.
- /// A task that executes when sockets are purged.
- public static async Task CleanStaleSockets(string directory, int probeTimeoutMs = DefaultProbeMs)
- {
- if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return;
-
- foreach (var file in Directory.EnumerateFiles(directory, "DalamudRPC.*.sock", SearchOption.TopDirectoryOnly))
- {
- // we don't need to check ourselves.
- if (file.Contains(Environment.ProcessId.ToString())) continue;
-
- bool shouldDelete;
-
- try
- {
- shouldDelete = !await IsSocketAlive(file, probeTimeoutMs);
- }
- catch
- {
- shouldDelete = true;
- }
-
- if (shouldDelete)
- {
- try
- {
- File.Delete(file);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not delete stale socket file: {File}", file);
- }
- }
- }
- }
-}
From c136934aa838fd1376e874117f39df0ce6702a61 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 26 Nov 2025 21:46:07 +0100
Subject: [PATCH 040/201] Always pass a key, even for release Fixes an issue
wherein the XL commandline parser wouldn't like the empty argument and error
out
---
Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs
index 51ff3bdcd..4e95b718e 100644
--- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs
@@ -105,7 +105,7 @@ public class BranchSwitcherWindow : Window
};
ps.ArgumentList.Add($"--dalamud-beta-kind={config.DalamudBetaKind}");
- ps.ArgumentList.Add($"--dalamud-beta-key={config.DalamudBetaKey}");
+ ps.ArgumentList.Add($"--dalamud-beta-key={(config.DalamudBetaKey.IsNullOrEmpty() ? "invalid" : config.DalamudBetaKey)}");
Process.Start(ps);
Environment.Exit(0);
From 4c3ba35f07a0fe9bf2a151f6734bdfbe7a4179db Mon Sep 17 00:00:00 2001
From: goaaats
Date: Thu, 27 Nov 2025 01:45:13 +0100
Subject: [PATCH 041/201] Don't inhibit ATK close events if pinned or
clickthrough windows are focused
---
Dalamud/Game/Internal/DalamudAtkTweaks.cs | 4 ++--
Dalamud/Interface/Internal/InterfaceManager.cs | 1 +
Dalamud/Interface/Windowing/Window.cs | 10 ++++++++++
Dalamud/Interface/Windowing/WindowSystem.cs | 13 ++++++++++++-
4 files changed, 25 insertions(+), 3 deletions(-)
diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs
index 486af463c..466401ef3 100644
--- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs
+++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs
@@ -113,7 +113,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
private void AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
// 3 == Close
- if (eventType == AtkEventType.InputReceived && WindowSystem.HasAnyWindowSystemFocus && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled)
+ if (eventType == AtkEventType.InputReceived && WindowSystem.ShouldInhibitAtkCloseEvents && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled)
{
Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return;
@@ -124,7 +124,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
private void AgentHudOpenSystemMenuDetour(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize)
{
- if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled)
+ if (WindowSystem.ShouldInhibitAtkCloseEvents && this.configuration.IsFocusManagementEnabled)
{
Log.Verbose($"Cancelling OpenSystemMenu due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return;
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index d68bc8bef..76a1b5172 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -1175,6 +1175,7 @@ internal partial class InterfaceManager : IInternalDisposableService
WindowSystem.HasAnyWindowSystemFocus = false;
WindowSystem.FocusedWindowSystemNamespace = string.Empty;
+ WindowSystem.ShouldInhibitAtkCloseEvents = false;
if (this.IsDispatchingEvents)
{
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index 44ff62199..f12e87099 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -226,6 +226,16 @@ public abstract class Window
///
public bool AllowClickthrough { get; set; } = true;
+ ///
+ /// Gets a value indicating whether this window is pinned.
+ ///
+ public bool IsPinned => this.internalIsPinned;
+
+ ///
+ /// Gets a value indicating whether this window is click-through.
+ ///
+ public bool IsClickthrough => this.internalIsClickthrough;
+
///
/// Gets or sets a list of available title bar buttons.
///
diff --git a/Dalamud/Interface/Windowing/WindowSystem.cs b/Dalamud/Interface/Windowing/WindowSystem.cs
index 87bd199a1..d6e9649bb 100644
--- a/Dalamud/Interface/Windowing/WindowSystem.cs
+++ b/Dalamud/Interface/Windowing/WindowSystem.cs
@@ -60,6 +60,12 @@ public class WindowSystem
///
public string? Namespace { get; set; }
+ ///
+ /// Gets or sets a value indicating whether ATK close events should be inhibited while any window has focus.
+ /// Does not respect windows that are pinned or clickthrough.
+ ///
+ internal static bool ShouldInhibitAtkCloseEvents { get; set; }
+
///
/// Add a window to this .
/// The window system doesn't own your window, it just renders it
@@ -130,7 +136,7 @@ public class WindowSystem
window.DrawInternal(flags, persistence);
}
- var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused && window.RespectCloseHotkey);
+ var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused);
this.HasAnyFocus = focusedWindow != default;
if (this.HasAnyFocus)
@@ -155,6 +161,11 @@ public class WindowSystem
}
}
+ ShouldInhibitAtkCloseEvents |= this.windows.Any(w => w.IsFocused &&
+ w.RespectCloseHotkey &&
+ !w.IsPinned &&
+ !w.IsClickthrough);
+
if (hasNamespace)
ImGui.PopID();
}
From c661faea6be8142a9005431ddaccf88ac2ce9025 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Wed, 12 Nov 2025 21:49:28 +0100
Subject: [PATCH 042/201] Fix services using wrong namespaces
---
.../Game/Addon/Events/AddonEventManagerAddressResolver.cs | 4 +++-
.../Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +-
Dalamud/Game/BaseAddressResolver.cs | 2 ++
Dalamud/Game/ClientState/ClientStateAddressResolver.cs | 2 ++
Dalamud/Game/ClientState/Objects/TargetManager.cs | 1 +
Dalamud/Game/Config/GameConfigAddressResolver.cs | 4 +++-
Dalamud/Game/DutyState/DutyStateAddressResolver.cs | 2 ++
Dalamud/Game/Gui/GameGuiAddressResolver.cs | 2 ++
Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs | 2 ++
Dalamud/Game/Network/GameNetworkAddressResolver.cs | 2 ++
.../Game/Network/Internal/NetworkHandlersAddressResolver.cs | 4 +++-
Dalamud/Game/SigScanner.cs | 2 ++
Dalamud/Game/TargetSigScanner.cs | 3 ++-
Dalamud/Plugin/{SelfTest => Services}/ISelfTestRegistry.cs | 4 ++--
Dalamud/Plugin/Services/ISigScanner.cs | 4 +---
Dalamud/Plugin/Services/ITargetManager.cs | 6 +++---
16 files changed, 33 insertions(+), 13 deletions(-)
rename Dalamud/Plugin/{SelfTest => Services}/ISelfTestRegistry.cs (95%)
diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
index 415e1b169..ec1c51a12 100644
--- a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
+++ b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
@@ -1,4 +1,6 @@
-namespace Dalamud.Game.Addon.Events;
+using Dalamud.Plugin.Services;
+
+namespace Dalamud.Game.Addon.Events;
///
/// AddonEventManager memory address resolver.
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
index 854d666fd..bc9e4b639 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
@@ -1,4 +1,4 @@
-using FFXIVClientStructs.FFXIV.Component.GUI;
+using Dalamud.Plugin.Services;
namespace Dalamud.Game.Addon.Lifecycle;
diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs
index 4133117d7..d41b1d9d8 100644
--- a/Dalamud/Game/BaseAddressResolver.cs
+++ b/Dalamud/Game/BaseAddressResolver.cs
@@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
+using Dalamud.Plugin.Services;
+
namespace Dalamud.Game;
///
diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
index 2fc859d09..53774121d 100644
--- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
+++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
@@ -1,3 +1,5 @@
+using Dalamud.Plugin.Services;
+
namespace Dalamud.Game.ClientState;
///
diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs
index f81154693..a6432e242 100644
--- a/Dalamud/Game/ClientState/Objects/TargetManager.cs
+++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs
@@ -1,6 +1,7 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs
index 2491c4033..e03f4f40b 100644
--- a/Dalamud/Game/Config/GameConfigAddressResolver.cs
+++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs
@@ -1,4 +1,6 @@
-namespace Dalamud.Game.Config;
+using Dalamud.Plugin.Services;
+
+namespace Dalamud.Game.Config;
///
/// Game config system address resolver.
diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs
index 1bca93efb..480b699a0 100644
--- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs
+++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs
@@ -1,3 +1,5 @@
+using Dalamud.Plugin.Services;
+
namespace Dalamud.Game.DutyState;
///
diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs
index 92b89c5a9..1295e2047 100644
--- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs
+++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs
@@ -1,3 +1,5 @@
+using Dalamud.Plugin.Services;
+
namespace Dalamud.Game.Gui;
///
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs
index 450e1fa9f..f97450c28 100644
--- a/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs
@@ -1,3 +1,5 @@
+using Dalamud.Plugin.Services;
+
namespace Dalamud.Game.Gui.NamePlate;
///
diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs
index de92f7c10..48abc2d97 100644
--- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs
+++ b/Dalamud/Game/Network/GameNetworkAddressResolver.cs
@@ -1,3 +1,5 @@
+using Dalamud.Plugin.Services;
+
namespace Dalamud.Game.Network;
///
diff --git a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs
index 9cd46f798..34c071556 100644
--- a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs
+++ b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs
@@ -1,4 +1,6 @@
-namespace Dalamud.Game.Network.Internal;
+using Dalamud.Plugin.Services;
+
+namespace Dalamud.Game.Network.Internal;
///
/// Internal address resolver for the network handlers.
diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs
index c8a371aee..262e98fa5 100644
--- a/Dalamud/Game/SigScanner.cs
+++ b/Dalamud/Game/SigScanner.cs
@@ -8,6 +8,8 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
+using Dalamud.Plugin.Services;
+
using Iced.Intel;
using Newtonsoft.Json;
using Serilog;
diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs
index f60c32d9a..540d0ea47 100644
--- a/Dalamud/Game/TargetSigScanner.cs
+++ b/Dalamud/Game/TargetSigScanner.cs
@@ -1,8 +1,9 @@
-using System.Diagnostics;
+using System.Diagnostics;
using System.IO;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
namespace Dalamud.Game;
diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/Services/ISelfTestRegistry.cs
similarity index 95%
rename from Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs
rename to Dalamud/Plugin/Services/ISelfTestRegistry.cs
index 7e9faf3f9..50d3d35ce 100644
--- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs
+++ b/Dalamud/Plugin/Services/ISelfTestRegistry.cs
@@ -1,8 +1,8 @@
using System.Collections.Generic;
-using Dalamud.Plugin.Services;
+using Dalamud.Plugin.SelfTest;
-namespace Dalamud.Plugin.SelfTest;
+namespace Dalamud.Plugin.Services;
///
/// Interface for registering and unregistering self-test steps from plugins.
diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs
index fbbd8b05a..017c4fe9d 100644
--- a/Dalamud/Plugin/Services/ISigScanner.cs
+++ b/Dalamud/Plugin/Services/ISigScanner.cs
@@ -2,9 +2,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
-using Dalamud.Plugin.Services;
-
-namespace Dalamud.Game;
+namespace Dalamud.Plugin.Services;
///
/// A SigScanner facilitates searching for memory signatures in a given ProcessModule.
diff --git a/Dalamud/Plugin/Services/ITargetManager.cs b/Dalamud/Plugin/Services/ITargetManager.cs
index 9c9fce550..0c14571c5 100644
--- a/Dalamud/Plugin/Services/ITargetManager.cs
+++ b/Dalamud/Plugin/Services/ITargetManager.cs
@@ -1,7 +1,7 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
-namespace Dalamud.Game.ClientState.Objects;
+namespace Dalamud.Plugin.Services;
///
/// Get and set various kinds of targets for the player.
@@ -37,13 +37,13 @@ public interface ITargetManager : IDalamudService
/// Set to null to clear the target.
///
public IGameObject? SoftTarget { get; set; }
-
+
///
/// Gets or sets the gpose target.
/// Set to null to clear the target.
///
public IGameObject? GPoseTarget { get; set; }
-
+
///
/// Gets or sets the mouseover nameplate target.
/// Set to null to clear the target.
From c525655be66778cd3d092d32f6ae5aba16c0fbea Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Thu, 27 Nov 2025 14:24:35 -0800
Subject: [PATCH 043/201] Improve LifecycleInvoke efficiency with Dictionary
---
.../Game/Addon/Lifecycle/AddonLifecycle.cs | 29 +++++++------------
1 file changed, 10 insertions(+), 19 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index 0c23f5661..cf1270803 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
///
/// Gets a list of all AddonLifecycle Event Listeners.
///
- internal List EventListeners { get; } = [];
+ internal Dictionary> EventListeners { get; } = [];
///
void IInternalDisposableService.DisposeService()
@@ -61,10 +61,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to register.
internal void RegisterListener(AddonLifecycleEventListener listener)
{
- this.framework.RunOnTick(() =>
- {
- this.EventListeners.Add(listener);
- });
+ this.EventListeners.TryAdd(listener.EventType, [ listener ]);
+ this.EventListeners[listener.EventType].Add(listener);
}
///
@@ -73,13 +71,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to unregister.
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
- // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
- listener.Removed = true;
-
- this.framework.RunOnTick(() =>
+ if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList))
{
- this.EventListeners.Remove(listener);
- });
+ listenerList.Remove(listener);
+ }
}
///
@@ -90,16 +85,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// What to blame on errors.
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
+ // Early return if we don't have any listeners of this type
+ if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return;
+
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
- foreach (var listener in this.EventListeners)
+ foreach (var listener in listenerList)
{
- if (listener.EventType != eventType)
- continue;
-
- // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener.
- if (listener.Removed)
- continue;
-
// Match on string.empty for listeners that want events for all addons.
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
From 166f249e13ed310db4bc44a658d8d473b92ae6a2 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Thu, 27 Nov 2025 14:30:40 -0800
Subject: [PATCH 044/201] Use hashset to prevent duplicate entries
---
Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index cf1270803..403671920 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
///
/// Gets a list of all AddonLifecycle Event Listeners.
///
- internal Dictionary> EventListeners { get; } = [];
+ internal Dictionary> EventListeners { get; } = [];
///
void IInternalDisposableService.DisposeService()
From 2a60bc61a7713172bf60ff1b2b78076b44473ac4 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Thu, 27 Nov 2025 15:52:18 -0800
Subject: [PATCH 045/201] Force style vars so erroring window renders at least
partially sanely
---
Dalamud/Interface/Windowing/Window.cs | 47 ++++++++++++++++++++++++---
1 file changed, 42 insertions(+), 5 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index f12e87099..5169b9746 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -57,6 +57,7 @@ public abstract class Window
private bool hasError = false;
private Exception? lastError;
+ private bool isErrorStylePushed;
///
/// Initializes a new instance of the class.
@@ -425,8 +426,16 @@ public abstract class Window
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
}
- this.PreDraw();
- this.ApplyConditionals();
+ if (!this.hasError)
+ {
+ this.PreDraw();
+ this.ApplyConditionals();
+ }
+ else
+ {
+ Style.StyleModelV1.DalamudStandard.Push();
+ this.isErrorStylePushed = true;
+ }
if (this.ForceMainWindow)
ImGuiHelpers.ForceNextWindowMainViewport();
@@ -448,10 +457,28 @@ public abstract class Window
var flags = this.Flags;
if (this.internalIsPinned || this.internalIsClickthrough)
- flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
+ {
+ if (!this.hasError)
+ {
+ flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
+ }
+ else
+ {
+ flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize);
+ }
+ }
if (this.internalIsClickthrough)
- flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
+ {
+ if (!this.hasError)
+ {
+ flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
+ }
+ else
+ {
+ flags &= ~(ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs);
+ }
+ }
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
{
@@ -670,7 +697,17 @@ public abstract class Window
Task.FromResult(tex));
}
- this.PostDraw();
+ if (!this.hasError)
+ {
+ this.PostDraw();
+ }
+ else
+ {
+ if (this.isErrorStylePushed)
+ {
+ Style.StyleModelV1.DalamudStandard.Pop();
+ }
+ }
this.PostHandlePreset(persistence);
From 29c154f9b5a2d7ab1cabd41cbaac432d06c2b27d Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 28 Nov 2025 08:35:54 -0800
Subject: [PATCH 046/201] Fix accidentally breaking widget
---
.../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
index c336f895e..73c4e540a 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle;
@@ -58,12 +57,11 @@ public class AddonLifecycleWidget : IDataWindowWidget
{
if (!this.Ready) return;
- foreach (var eventType in Enum.GetValues())
+ foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners)
{
- if (ImGui.CollapsingHeader(eventType.ToString()))
+ if (ImGui.CollapsingHeader(listenerType.ToString()))
{
ImGui.Indent();
- var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList();
if (listeners.Count == 0)
{
From 325d28ee3211d7743fc407f9f934e4bbc66ec48a Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 28 Nov 2025 09:08:24 -0800
Subject: [PATCH 047/201] further improve performance
---
.../Game/Addon/Lifecycle/AddonLifecycle.cs | 61 ++++++++++++++-----
.../Data/Widgets/AddonLifecycleWidget.cs | 40 ++++++------
2 files changed, 67 insertions(+), 34 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index 403671920..e38f56921 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -38,7 +38,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
///
/// Gets a list of all AddonLifecycle Event Listeners.
///
- internal Dictionary> EventListeners { get; } = [];
+ /// Mapping is: EventType -> AddonName -> ListenerList
+ internal Dictionary>> EventListeners { get; } = [];
///
void IInternalDisposableService.DisposeService()
@@ -61,8 +62,18 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to register.
internal void RegisterListener(AddonLifecycleEventListener listener)
{
- this.EventListeners.TryAdd(listener.EventType, [ listener ]);
- this.EventListeners[listener.EventType].Add(listener);
+ if (!this.EventListeners.ContainsKey(listener.EventType))
+ {
+ this.EventListeners.TryAdd(listener.EventType, []);
+ }
+
+ // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
+ if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
+ {
+ this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []);
+ }
+
+ this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
}
///
@@ -71,9 +82,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to unregister.
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
- if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList))
+ if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
{
- listenerList.Remove(listener);
+ if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
+ {
+ addonListener.Remove(listener);
+ }
}
}
@@ -86,22 +100,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
// Early return if we don't have any listeners of this type
- if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return;
+ if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
- // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
- foreach (var listener in listenerList)
+ // Handle listeners for this event type that don't care which addon is triggering it
+ if (addonListeners.TryGetValue(string.Empty, out var globalListeners))
{
- // Match on string.empty for listeners that want events for all addons.
- if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
- continue;
-
- try
+ foreach (var listener in globalListeners)
{
- listener.FunctionDelegate.Invoke(eventType, args);
+ try
+ {
+ listener.FunctionDelegate.Invoke(eventType, args);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
+ }
}
- catch (Exception e)
+ }
+
+ // Handle listeners that are listening for this addon and event type specifically
+ if (addonListeners.TryGetValue(args.AddonName, out var addonListener))
+ {
+ foreach (var listener in addonListener)
{
- Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
+ try
+ {
+ listener.FunctionDelegate.Invoke(eventType, args);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
+ }
}
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
index 73c4e540a..0f193556b 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -2,7 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle;
-using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Utility;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@@ -57,35 +58,38 @@ public class AddonLifecycleWidget : IDataWindowWidget
{
if (!this.Ready) return;
- foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners)
+ foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners)
{
- if (ImGui.CollapsingHeader(listenerType.ToString()))
+ using var eventId = ImRaii.PushId(eventType.ToString());
+
+ if (ImGui.CollapsingHeader(eventType.ToString()))
{
- ImGui.Indent();
+ using var eventIndent = ImRaii.PushIndent();
- if (listeners.Count == 0)
+ if (addonListeners.Count == 0)
{
- ImGui.Text("No Listeners Registered for Event"u8);
+ ImGui.Text("No Addons Registered for Event"u8);
}
- if (ImGui.BeginTable("AddonLifecycleListenersTable"u8, 2))
+ foreach (var (addonName, listeners) in addonListeners)
{
- ImGui.TableSetupColumn("##AddonName"u8, ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale);
- ImGui.TableSetupColumn("##MethodInvoke"u8, ImGuiTableColumnFlags.WidthStretch);
+ using var addonId = ImRaii.PushId(addonName);
- foreach (var listener in listeners)
+ if (ImGui.CollapsingHeader(addonName.IsNullOrEmpty() ? "GLOBAL" : addonName))
{
- ImGui.TableNextColumn();
- ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName);
+ using var addonIndent = ImRaii.PushIndent();
- ImGui.TableNextColumn();
- ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}");
+ if (listeners.Count == 0)
+ {
+ ImGui.Text("No Listeners Registered for Event"u8);
+ }
+
+ foreach (var listener in listeners)
+ {
+ ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}");
+ }
}
-
- ImGui.EndTable();
}
-
- ImGui.Unindent();
}
}
}
From 170f6e08599d4853acc260aa3a442171d30a1731 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 28 Nov 2025 09:11:13 -0800
Subject: [PATCH 048/201] Remove redundant header
---
.../Windows/Data/Widgets/AddonLifecycleWidget.cs | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
index 0f193556b..4fb13b81a 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -46,18 +46,6 @@ public class AddonLifecycleWidget : IDataWindowWidget
return;
}
- if (ImGui.CollapsingHeader("Listeners"u8))
- {
- ImGui.Indent();
- this.DrawEventListeners();
- ImGui.Unindent();
- }
- }
-
- private void DrawEventListeners()
- {
- if (!this.Ready) return;
-
foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners)
{
using var eventId = ImRaii.PushId(eventType.ToString());
From d7915c7020f04f15eaa4caa2c341b2356c51a08f Mon Sep 17 00:00:00 2001
From: goaaats
Date: Fri, 28 Nov 2025 18:11:31 +0100
Subject: [PATCH 049/201] Show a sensible error message when Lumina fails to
init
---
Dalamud/Data/DataManager.cs | 33 +++++++++++++++++++++++----------
1 file changed, 23 insertions(+), 10 deletions(-)
diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs
index d017bf85a..ed0aa6c4d 100644
--- a/Dalamud/Data/DataManager.cs
+++ b/Dalamud/Data/DataManager.cs
@@ -41,7 +41,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
try
{
Log.Verbose("Starting data load...");
-
+
using (Timings.Start("Lumina Init"))
{
var luminaOptions = new LuminaOptions
@@ -53,12 +53,25 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
DefaultExcelLanguage = this.Language.ToLumina(),
};
- this.GameData = new(
- Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
- luminaOptions)
+ try
{
- StreamPool = new(),
- };
+ this.GameData = new(
+ Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
+ luminaOptions)
+ {
+ StreamPool = new(),
+ };
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Lumina GameData init failed");
+ Util.Fatal(
+ "Dalamud could not read required game data files. This likely means your game installation is corrupted or incomplete.\n\n" +
+ "Please repair your installation by right-clicking the login button in XIVLauncher and choosing \"Repair game files\".",
+ "Dalamud");
+
+ return;
+ }
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
@@ -71,7 +84,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
dalamud.StartInfo.TroubleshootingPackData);
this.HasModifiedGameDataFiles =
tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
-
+
if (this.HasModifiedGameDataFiles)
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);
}
@@ -130,7 +143,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
#region Lumina Wrappers
///
- public ExcelSheet GetExcelSheet(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow
+ public ExcelSheet GetExcelSheet(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow
=> this.Excel.GetSheet(language?.ToLumina(), name);
///
@@ -138,7 +151,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
=> this.Excel.GetSubrowSheet(language?.ToLumina(), name);
///
- public FileResource? GetFile(string path)
+ public FileResource? GetFile(string path)
=> this.GetFile(path);
///
@@ -161,7 +174,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
: Task.FromException(new FileNotFoundException("The file could not be found."));
///
- public bool FileExists(string path)
+ public bool FileExists(string path)
=> this.GameData.FileExists(path);
#endregion
From b8724f7a59b5cb3dd0b454dff384b7c0c7b0d355 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 28 Nov 2025 09:44:35 -0800
Subject: [PATCH 050/201] Fix copy paste error
---
Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +-
Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index e38f56921..d3d0fcebe 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -37,7 +37,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
///
/// Gets a list of all AddonLifecycle Event Listeners.
- ///
+ ///
/// Mapping is: EventType -> AddonName -> ListenerList
internal Dictionary>> EventListeners { get; } = [];
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index 54c91248e..db698e626 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -167,7 +167,7 @@ internal unsafe class AddonVirtualTable : IDisposable
this.addonFinalizeArg.Clear();
this.addonFinalizeArg.Addon = thisPtr;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg);
try
{
From 78ecb721cd3f48fd06d72b1d850a47ff68f39d1f Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Sat, 29 Nov 2025 12:47:45 +0000
Subject: [PATCH 051/201] Update ClientStructs
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index 0769d1f18..e5f586630 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit 0769d1f180f859688f47a7a99610e9ce10da946c
+Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77
From d12a9ec7da4c2e91c22c0d3f848197eb743ba03d Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 29 Nov 2025 19:15:37 +0100
Subject: [PATCH 052/201] Remove DalamudBetaKey, DalamudBetaKind from config
Fix all code that depends on it to use Util.GetActiveTrack() instead
---
.../Internal/DalamudConfiguration.cs | 10 ----------
Dalamud/Interface/Internal/DalamudInterface.cs | 2 +-
.../Internal/Windows/BranchSwitcherWindow.cs | 18 ++++++++----------
.../PluginInstaller/PluginInstallerWindow.cs | 9 ++++-----
Dalamud/Support/DalamudReleases.cs | 15 ++++++++++-----
Dalamud/Support/Troubleshooting.cs | 2 +-
6 files changed, 24 insertions(+), 32 deletions(-)
diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index da0d7c2c6..9404b5b10 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -108,11 +108,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
///
public bool DoPluginTest { get; set; } = false;
- ///
- /// Gets or sets a key to opt into Dalamud staging builds.
- ///
- public string? DalamudBetaKey { get; set; } = null;
-
///
/// Gets or sets a list of custom repos.
///
@@ -278,11 +273,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
///
public bool IsResumeGameAfterPluginLoad { get; set; } = false;
- ///
- /// Gets or sets the kind of beta to download when matches the server value.
- ///
- public string? DalamudBetaKind { get; set; }
-
///
/// Gets or sets a value indicating whether any plugin should be loaded when the game is started.
/// It is reset immediately when read.
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index 202334580..64e1acaa4 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -182,7 +182,7 @@ internal class DalamudInterface : IInternalDisposableService
() => Service.GetNullable()?.ToggleDevMenu(),
VirtualKey.SHIFT);
- if (!configuration.DalamudBetaKind.IsNullOrEmpty())
+ if (Util.GetActiveTrack() != "release")
{
titleScreenMenu.AddEntryCore(
Loc.Localize("TSMDalamudDevMenu", "Developer Menu"),
diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs
index 4e95b718e..9cc14ea14 100644
--- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs
@@ -6,7 +6,6 @@ using System.Net.Http.Json;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
-using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
@@ -15,6 +14,8 @@ using Dalamud.Utility;
using Newtonsoft.Json;
+using Serilog;
+
namespace Dalamud.Interface.Internal.Windows;
///
@@ -47,7 +48,7 @@ public class BranchSwitcherWindow : Window
Debug.Assert(this.branches != null, "this.branches != null");
var trackName = Util.GetActiveTrack();
- this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track != trackName);
+ this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track == trackName);
if (this.selectedBranchIndex == -1)
{
this.selectedBranchIndex = 0;
@@ -86,12 +87,9 @@ public class BranchSwitcherWindow : Window
if (ImGui.Button("Pick & Restart"u8))
{
- var config = Service.Get();
- config.DalamudBetaKind = pickedBranch.Key;
- config.DalamudBetaKey = pickedBranch.Value.Key;
-
- // If we exit immediately, we need to write out the new config now
- config.ForceSave();
+ var newTrackName = pickedBranch.Key;
+ var newTrackKey = pickedBranch.Value.Key;
+ Log.Verbose("Switching to branch {Branch} with key {Key}", newTrackName, newTrackKey);
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var xlPath = Path.Combine(appData, "XIVLauncher", "current", "XIVLauncher.exe");
@@ -104,8 +102,8 @@ public class BranchSwitcherWindow : Window
UseShellExecute = false,
};
- ps.ArgumentList.Add($"--dalamud-beta-kind={config.DalamudBetaKind}");
- ps.ArgumentList.Add($"--dalamud-beta-key={(config.DalamudBetaKey.IsNullOrEmpty() ? "invalid" : config.DalamudBetaKey)}");
+ ps.ArgumentList.Add($"--dalamud-beta-kind={newTrackName}");
+ ps.ArgumentList.Add($"--dalamud-beta-key={(newTrackKey.IsNullOrEmpty() ? "invalid" : newTrackKey)}");
Process.Start(ps);
Environment.Exit(0);
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index b203b3894..ac092bd25 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -302,8 +302,7 @@ internal class PluginInstallerWindow : Window, IDisposable
this.profileManagerWidget.Reset();
- var config = Service.Get();
- if (this.staleDalamudNewVersion == null && !config.DalamudBetaKind.IsNullOrEmpty())
+ if (this.staleDalamudNewVersion == null && !Util.GetActiveTrack().IsNullOrEmpty())
{
Service.Get().GetVersionForCurrentTrack().ContinueWith(t =>
{
@@ -311,10 +310,10 @@ internal class PluginInstallerWindow : Window, IDisposable
return;
var versionInfo = t.Result;
- if (versionInfo.AssemblyVersion != Util.GetScmVersion() &&
- versionInfo.Track != "release" &&
- string.Equals(versionInfo.Key, config.DalamudBetaKey, StringComparison.OrdinalIgnoreCase))
+ if (versionInfo.AssemblyVersion != Util.GetScmVersion())
+ {
this.staleDalamudNewVersion = versionInfo.AssemblyVersion;
+ }
});
}
}
diff --git a/Dalamud/Support/DalamudReleases.cs b/Dalamud/Support/DalamudReleases.cs
index 15e851da2..603c77487 100644
--- a/Dalamud/Support/DalamudReleases.cs
+++ b/Dalamud/Support/DalamudReleases.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Networking.Http;
+using Dalamud.Utility;
using Newtonsoft.Json;
@@ -15,7 +16,7 @@ namespace Dalamud.Support;
internal class DalamudReleases : IServiceType
{
private const string VersionInfoUrl = "https://kamori.goats.dev/Dalamud/Release/VersionInfo?track={0}";
-
+
private readonly HappyHttpClient httpClient;
private readonly DalamudConfiguration config;
@@ -30,20 +31,24 @@ internal class DalamudReleases : IServiceType
this.httpClient = httpClient;
this.config = config;
}
-
+
///
/// Get the latest version info for the current track.
///
/// The version info for the current track.
- public async Task GetVersionForCurrentTrack()
+ public async Task GetVersionForCurrentTrack()
{
- var url = string.Format(VersionInfoUrl, [this.config.DalamudBetaKind]);
+ var currentTrack = Util.GetActiveTrack();
+ if (currentTrack.IsNullOrEmpty())
+ return null;
+
+ var url = string.Format(VersionInfoUrl, [currentTrack]);
var response = await this.httpClient.SharedHttpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject(content);
}
-
+
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "laziness")]
public class DalamudVersionInfo
{
diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs
index 4af8d5ffc..88048c462 100644
--- a/Dalamud/Support/Troubleshooting.cs
+++ b/Dalamud/Support/Troubleshooting.cs
@@ -73,7 +73,7 @@ public static class Troubleshooting
DalamudGitHash = Util.GetGitHash() ?? "Unknown",
GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown",
Language = startInfo.Language.ToString(),
- BetaKey = configuration.DalamudBetaKey,
+ BetaKey = Util.GetActiveTrack(),
DoPluginTest = configuration.DoPluginTest,
LoadAllApiLevels = pluginManager?.LoadAllApiLevels == true,
InterfaceLoaded = interfaceManager?.IsReady ?? false,
From 7510c032cc0e706787259b44d4ab2415cc74c8a3 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 29 Nov 2025 19:22:28 +0100
Subject: [PATCH 053/201] Disable Intel CET support, causes CLR crashes on
unpatched Windows
---
Directory.Build.props | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Directory.Build.props b/Directory.Build.props
index 4ed87c809..f9f061c17 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,6 +6,10 @@
x64
x64
13.0
+
+
+
+ false
From fadf941fa47f5d8775f157a64a5414bfcb00faab Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 30 Nov 2025 02:01:01 +0100
Subject: [PATCH 054/201] Re-add config properties for XLCore/XoM backwards
compatibility
---
Dalamud/Configuration/Internal/DalamudConfiguration.cs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 9404b5b10..d546dc517 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -487,6 +487,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
///
public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f);
+#pragma warning disable SA1600
+#pragma warning disable SA1516
+ // XLCore/XoM compatibility until they move it out
+ public string? DalamudBetaKey { get; set; } = null;
+ public string? DalamudBetaKind { get; set; }
+#pragma warning restore SA1516
+#pragma warning restore SA1600
+
///
/// Load a configuration from the provided path.
///
From ead1c705a427ee596ca5a4eff741b0d68fe07fc0 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Sat, 29 Nov 2025 17:07:51 -0800
Subject: [PATCH 055/201] fix: Route URIs to the specified InternalName
---
Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs
index 4dbe3fdf1..3b7f18437 100644
--- a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs
+++ b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs
@@ -45,7 +45,7 @@ public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler
private void HandleUri(DalamudUri uri)
{
- var target = uri.Path.Split("/").FirstOrDefault();
+ var target = uri.Path.Split("/").ElementAtOrDefault(1);
var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName);
if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase))
{
From ac2d522415b9a5ccec7a8c5cead997413412a699 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 30 Nov 2025 02:47:07 +0100
Subject: [PATCH 056/201] build: 13.0.0.12
---
Dalamud/Dalamud.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index ce140b8c9..d1f730d5e 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -6,7 +6,7 @@
XIV Launcher addon framework
- 13.0.0.11
+ 13.0.0.12
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
From 874745651b1be57b391a078c627a66d7932e30aa Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Sat, 29 Nov 2025 21:07:51 -0800
Subject: [PATCH 057/201] feat: Add PID, process time, rename ClientIdentifer
to ClientState
---
.../Rpc/Service/ClientHelloService.cs | 21 +++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs
index 041bc135f..c5a4c851a 100644
--- a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs
+++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs
@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System.Diagnostics;
+using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Game;
@@ -39,7 +40,9 @@ internal sealed class ClientHelloService : IInternalDisposableService
ApiVersion = "1.0",
DalamudVersion = Util.GetScmVersion(),
GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown",
- ClientIdentifier = await this.GetClientIdentifier(),
+ ProcessId = Environment.ProcessId,
+ ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(),
+ ClientState = await this.GetClientIdentifier(),
};
}
@@ -114,7 +117,17 @@ internal record ClientHelloResponse
public string? GameVersion { get; init; }
///
- /// Gets an identifier for this client.
+ /// Gets the process ID of this client.
///
- public string? ClientIdentifier { get; init; }
+ public int? ProcessId { get; init; }
+
+ ///
+ /// Gets the time this process started.
+ ///
+ public long? ProcessStartTime { get; init; }
+
+ ///
+ /// Gets a state for this client for user display.
+ ///
+ public string? ClientState { get; init; }
}
From c51e65e0bd07bc58bfab65128cb7719ce56c996b Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 10:08:40 -0800
Subject: [PATCH 058/201] Better unload
---
.../Game/Addon/Lifecycle/AddonLifecycle.cs | 33 +++++--------------
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 15 +++------
2 files changed, 14 insertions(+), 34 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index d3d0fcebe..5d121bea4 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -19,13 +19,13 @@ namespace Dalamud.Game.Addon.Lifecycle;
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IInternalDisposableService
{
+ ///
+ /// Gets a list of all allocated addon virtual tables.
+ ///
+ public static readonly List AllocatedTables = [];
+
private static readonly ModuleLog Log = new("AddonLifecycle");
- [ServiceManager.ServiceDependency]
- private readonly Framework framework = Service.Get();
-
- private readonly Dictionary modifiedTables = [];
-
private Hook? onInitializeAddonHook;
[ServiceManager.ServiceConstructor]
@@ -47,13 +47,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
this.onInitializeAddonHook?.Dispose();
this.onInitializeAddonHook = null;
- this.framework.RunOnFrameworkThread(() =>
- {
- foreach (var virtualTable in this.modifiedTables.Values)
- {
- virtualTable.Dispose();
- }
- });
+ AllocatedTables.ForEach(entry => entry.Dispose());
+ AllocatedTables.Clear();
}
///
@@ -141,18 +136,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{
this.LogInitialize(addon->NameString);
- if (!this.modifiedTables.ContainsKey(addon->NameString))
- {
- // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
- var managedVirtualTableEntry = new AddonVirtualTable(addon, this)
- {
- // This event is invoked when the game itself has disposed of an addon
- // We can use this to know when to remove our virtual table entry
- OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString),
- };
-
- this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry);
- }
+ // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
+ AllocatedTables.Add(new AddonVirtualTable(addon, this));
}
catch (Exception e)
{
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index db698e626..d91cd648f 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Logging.Internal;
@@ -108,17 +109,11 @@ internal unsafe class AddonVirtualTable : IDisposable
this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
}
- ///
- /// Gets an event that is invoked when this addon's Finalize method is called from native.
- ///
- public required Action OnAddonFinalized { get; init; }
-
- ///
- /// WARNING! This should not be called at any time except during dalamud unload.
- ///
+ ///
public void Dispose()
{
- this.atkUnitBase->VirtualTable = this.originalVirtualTable;
+ // Ensure restoration is done atomically.
+ Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable);
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
}
@@ -131,7 +126,7 @@ internal unsafe class AddonVirtualTable : IDisposable
if ((freeFlags & 1) == 1)
{
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
- this.OnAddonFinalized();
+ AddonLifecycle.AllocatedTables.Remove(this);
}
return result;
From 26f119096bad6fd3111a6c5f0ad977f53e396384 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 10:39:35 -0800
Subject: [PATCH 059/201] Bunch of stuff...
---
Dalamud/Configuration/PluginConfigurations.cs | 2 +-
.../Lifecycle/AddonArgTypes/AddonArgs.cs | 31 ++-----------------
.../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 9 ++++--
.../AddonArgTypes/AddonFinalizeArgs.cs | 7 +++--
.../AddonArgTypes/AddonGenericArgs.cs | 3 +-
.../AddonArgTypes/AddonReceiveEventArgs.cs | 18 +++--------
.../AddonArgTypes/AddonRefreshArgs.cs | 15 +++------
.../AddonArgTypes/AddonRequestedUpdateArgs.cs | 11 +------
.../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 15 +++------
.../AddonArgTypes/AddonUpdateArgs.cs | 26 +++++++---------
.../AddonLifecycleAddressResolver.cs | 2 +-
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 14 ---------
Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +-
Dalamud/Interface/Animation/Easing.cs | 2 +-
...ToDoAttribute.cs => Api14ToDoAttribute.cs} | 6 ++--
Dalamud/Utility/Api15ToDoAttribute.cs | 25 +++++++++++++++
Dalamud/Utility/Util.cs | 2 +-
17 files changed, 74 insertions(+), 116 deletions(-)
rename Dalamud/Utility/{Api13ToDoAttribute.cs => Api14ToDoAttribute.cs} (75%)
create mode 100644 Dalamud/Utility/Api15ToDoAttribute.cs
diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs
index fa2969d31..c01ab2af0 100644
--- a/Dalamud/Configuration/PluginConfigurations.cs
+++ b/Dalamud/Configuration/PluginConfigurations.cs
@@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
///
/// Configuration to store settings for a dalamud plugin.
///
-[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
+[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
public sealed class PluginConfigurations
{
private readonly DirectoryInfo configDirectory;
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
index 0b2ae1178..62ca47238 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
@@ -33,41 +33,14 @@ public abstract class AddonArgs
///
public abstract AddonArgsType Type { get; }
- ///
- /// Checks if addon name matches the given span of char.
- ///
- /// The name to check.
- /// Whether it is the case.
- internal bool IsAddon(string name)
- {
- if (this.Addon.IsNull)
- return false;
-
- if (name.Length is 0 or > 32)
- return false;
-
- if (string.IsNullOrEmpty(this.Addon.Name))
- return false;
-
- return name == this.Addon.Name;
- }
-
- ///
- /// Clears this AddonArgs values.
- ///
- internal virtual void Clear()
- {
- this.addonName = null;
- this.Addon = 0;
- }
-
///
/// Helper method for ensuring the name of the addon is valid.
///
/// The name of the addon for this object. when invalid.
private string GetAddonName()
{
- if (this.Addon.IsNull) return InvalidAddon;
+ if (this.Addon.IsNull)
+ return InvalidAddon;
var name = this.Addon.Name;
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
index 7254ba7b3..a834d2983 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
@@ -1,15 +1,18 @@
-namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Utility;
+
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Draw events.
///
+[Obsolete("Use AddonGenericArgs instead.")]
+[Api15ToDo("Remove this")]
public class AddonDrawArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonDrawArgs()
+ internal AddonDrawArgs()
{
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
index 12def3ad3..11d15a081 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
@@ -1,15 +1,18 @@
+using Dalamud.Utility;
+
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for ReceiveEvent events.
///
+[Obsolete("Use AddonGenericArgs instead.")]
+[Api15ToDo("Remove this")]
public class AddonFinalizeArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonFinalizeArgs()
+ internal AddonFinalizeArgs()
{
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
index f3078af69..a20e9d23b 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
@@ -8,8 +8,7 @@ public class AddonGenericArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonGenericArgs()
+ internal AddonGenericArgs()
{
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
index 05f51b118..bb8168075 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
@@ -1,3 +1,5 @@
+using Dalamud.Utility;
+
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
@@ -8,8 +10,7 @@ public class AddonReceiveEventArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonReceiveEventArgs()
+ internal AddonReceiveEventArgs()
{
}
@@ -32,17 +33,8 @@ public class AddonReceiveEventArgs : AddonArgs
public nint AtkEvent { get; set; }
///
- /// Gets or sets the pointer to a block of data for this event message.
+ /// Gets or sets the pointer to an AtkEventData for this event message.
///
+ [Api14ToDo("Rename to AtkEventData")]
public nint Data { get; set; }
-
- ///
- internal override void Clear()
- {
- base.Clear();
- this.AtkEventType = 0;
- this.EventParam = 0;
- this.AtkEvent = 0;
- this.Data = 0;
- }
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
index c01c065c1..8af017318 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
@@ -1,3 +1,5 @@
+using Dalamud.Utility;
+
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@@ -10,8 +12,7 @@ public class AddonRefreshArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonRefreshArgs()
+ internal AddonRefreshArgs()
{
}
@@ -31,13 +32,7 @@ public class AddonRefreshArgs : AddonArgs
///
/// Gets the AtkValues in the form of a span.
///
+ [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")]
+ [Api15ToDo("Remove this")]
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
-
- ///
- internal override void Clear()
- {
- base.Clear();
- this.AtkValueCount = 0;
- this.AtkValues = 0;
- }
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
index bf00c5d6e..7005b77c2 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
@@ -8,8 +8,7 @@ public class AddonRequestedUpdateArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonRequestedUpdateArgs()
+ internal AddonRequestedUpdateArgs()
{
}
@@ -25,12 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs
/// Gets or sets the StringArrayData** for this event.
///
public nint StringArrayData { get; set; }
-
- ///
- internal override void Clear()
- {
- base.Clear();
- this.NumberArrayData = 0;
- this.StringArrayData = 0;
- }
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
index 9b7e86a61..9fd7b6dd0 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
@@ -1,3 +1,5 @@
+using Dalamud.Utility;
+
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@@ -10,8 +12,7 @@ public class AddonSetupArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonSetupArgs()
+ internal AddonSetupArgs()
{
}
@@ -31,13 +32,7 @@ public class AddonSetupArgs : AddonArgs
///
/// Gets the AtkValues in the form of a span.
///
+ [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")]
+ [Api15ToDo("Remove this")]
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
-
- ///
- internal override void Clear()
- {
- base.Clear();
- this.AtkValueCount = 0;
- this.AtkValues = 0;
- }
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
index bab62fc89..e6147d0eb 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
@@ -1,39 +1,35 @@
+using Dalamud.Utility;
+
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Update events.
///
+[Obsolete("Use AddonGenericArgs instead.")]
+[Api15ToDo("Remove this")]
public class AddonUpdateArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonUpdateArgs()
+ internal AddonUpdateArgs()
{
}
///
public override AddonArgsType Type => AddonArgsType.Update;
- ///
- /// Gets the time since the last update.
- ///
- public float TimeDelta
- {
- get => this.TimeDeltaInternal;
- init => this.TimeDeltaInternal = value;
- }
-
///
/// Gets or sets the time since the last update.
///
internal float TimeDeltaInternal { get; set; }
- ///
- internal override void Clear()
+ ///
+ /// Gets the time since the last update.
+ ///
+ private float TimeDelta
{
- base.Clear();
- this.TimeDeltaInternal = 0;
+ get => this.TimeDeltaInternal;
+ init => this.TimeDeltaInternal = value;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
index 9359870a5..2fa3c5b91 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
///
/// AddonLifecycleService memory address resolver.
///
-[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")]
+[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")]
internal class AddonLifecycleAddressResolver : BaseAddressResolver
{
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index d91cd648f..49ffdc7fb 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -26,8 +26,6 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AddonLifecycle lifecycleService;
- // Obsolete warning is only to prevent users from creating their own event objects.
-#pragma warning disable CS0618 // Type or member is obsolete
private readonly AddonSetupArgs addonSetupArg = new();
private readonly AddonFinalizeArgs addonFinalizeArg = new();
private readonly AddonDrawArgs addonDrawArg = new();
@@ -36,7 +34,6 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new();
private readonly AddonReceiveEventArgs addonReceiveEventArg = new();
private readonly AddonGenericArgs addonGenericArg = new();
-#pragma warning restore CS0618 // Type or member is obsolete
private readonly AtkUnitBase* atkUnitBase;
@@ -136,7 +133,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonSetupArg.Clear();
this.addonSetupArg.Addon = addon;
this.addonSetupArg.AtkValueCount = valueCount;
this.addonSetupArg.AtkValues = (nint)values;
@@ -160,7 +156,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonFinalizeArg.Clear();
this.addonFinalizeArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg);
@@ -178,7 +173,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonDrawArg.Clear();
this.addonDrawArg.Addon = addon;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg);
@@ -198,7 +192,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonUpdateArg.Clear();
this.addonUpdateArg.Addon = addon;
this.addonUpdateArg.TimeDeltaInternal = delta;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg);
@@ -221,7 +214,6 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
- this.addonRefreshArg.Clear();
this.addonRefreshArg.Addon = addon;
this.addonRefreshArg.AtkValueCount = valueCount;
this.addonRefreshArg.AtkValues = (nint)values;
@@ -246,7 +238,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonRequestedUpdateArg.Clear();
this.addonRequestedUpdateArg.Addon = addon;
this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData;
this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData;
@@ -270,7 +261,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonReceiveEventArg.Clear();
this.addonReceiveEventArg.Addon = (nint)addon;
this.addonReceiveEventArg.AtkEventType = (byte)eventType;
this.addonReceiveEventArg.EventParam = eventParam;
@@ -300,7 +290,6 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
- this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg);
@@ -324,7 +313,6 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
- this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg);
@@ -346,7 +334,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg);
@@ -366,7 +353,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg);
diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
index f5b7011fe..af85f9228 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
@@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
}
///
- [Api13ToDo("Maybe make this config scoped to internal name?")]
+ [Api14ToDo("Maybe make this config scoped to internal name?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
///
diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs
index 0d2057b3b..cc1f48ce7 100644
--- a/Dalamud/Interface/Animation/Easing.cs
+++ b/Dalamud/Interface/Animation/Easing.cs
@@ -48,7 +48,7 @@ public abstract class Easing
/// Gets the current value of the animation, following unclamped logic.
///
[Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)]
- [Api13ToDo("Map this field to ValueClamped, probably.")]
+ [Api14ToDo("Map this field to ValueClamped, probably.")]
public double Value => this.ValueUnclamped;
///
diff --git a/Dalamud/Utility/Api13ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs
similarity index 75%
rename from Dalamud/Utility/Api13ToDoAttribute.cs
rename to Dalamud/Utility/Api14ToDoAttribute.cs
index 576401cda..945b6e4db 100644
--- a/Dalamud/Utility/Api13ToDoAttribute.cs
+++ b/Dalamud/Utility/Api14ToDoAttribute.cs
@@ -4,7 +4,7 @@ namespace Dalamud.Utility;
/// Utility class for marking something to be changed for API 13, for ease of lookup.
///
[AttributeUsage(AttributeTargets.All, Inherited = false)]
-internal sealed class Api13ToDoAttribute : Attribute
+internal sealed class Api14ToDoAttribute : Attribute
{
///
/// Marks that this should be made internal.
@@ -12,11 +12,11 @@ internal sealed class Api13ToDoAttribute : Attribute
public const string MakeInternal = "Make internal.";
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The explanation.
/// The explanation 2.
- public Api13ToDoAttribute(string what, string what2 = "")
+ public Api14ToDoAttribute(string what, string what2 = "")
{
_ = what;
_ = what2;
diff --git a/Dalamud/Utility/Api15ToDoAttribute.cs b/Dalamud/Utility/Api15ToDoAttribute.cs
new file mode 100644
index 000000000..646c260e8
--- /dev/null
+++ b/Dalamud/Utility/Api15ToDoAttribute.cs
@@ -0,0 +1,25 @@
+namespace Dalamud.Utility;
+
+///
+/// Utility class for marking something to be changed for API 13, for ease of lookup.
+/// Intended to represent not the upcoming API, but the one after it for more major changes.
+///
+[AttributeUsage(AttributeTargets.All, Inherited = false)]
+internal sealed class Api15ToDoAttribute : 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 Api15ToDoAttribute(string what, string what2 = "")
+ {
+ _ = what;
+ _ = what2;
+ }
+}
diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs
index 2a3733303..ba31f47e5 100644
--- a/Dalamud/Utility/Util.cs
+++ b/Dalamud/Utility/Util.cs
@@ -79,7 +79,7 @@ public static partial class Util
///
/// Gets the Dalamud version.
///
- [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")]
+ [Api14ToDo("Remove. Make both versions here internal. Add an API somewhere.")]
public static string AssemblyVersion { get; } =
Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString();
From 54bac7f32a7efe5268d8fa3697f185184134bab7 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Tue, 25 Nov 2025 17:27:48 -0800
Subject: [PATCH 060/201] Refactor Addon Lifecycle
---
.../Game/Addon/AddonLifecyclePooledArgs.cs | 107 -----
.../Lifecycle/AddonArgTypes/AddonArgs.cs | 2 +-
.../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +-
.../AddonArgTypes/AddonFinalizeArgs.cs | 8 +-
.../AddonArgTypes/AddonGenericArgs.cs | 18 +
.../AddonArgTypes/AddonReceiveEventArgs.cs | 16 +-
.../AddonArgTypes/AddonRefreshArgs.cs | 12 +-
.../AddonArgTypes/AddonRequestedUpdateArgs.cs | 12 +-
.../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 12 +-
.../AddonArgTypes/AddonUpdateArgs.cs | 10 +-
Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +-
Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 54 ++-
.../Game/Addon/Lifecycle/AddonLifecycle.cs | 298 ++-----------
.../AddonLifecycleAddressResolver.cs | 38 +-
.../AddonLifecycleReceiveEventListener.cs | 112 -----
.../Game/Addon/Lifecycle/AddonSetupHook.cs | 80 ----
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 405 ++++++++++++++++++
Dalamud/Hooking/Internal/CallHook.cs | 100 -----
.../Data/Widgets/AddonLifecycleWidget.cs | 51 ---
19 files changed, 543 insertions(+), 825 deletions(-)
delete mode 100644 Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs
create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
delete mode 100644 Dalamud/Hooking/Internal/CallHook.cs
diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
deleted file mode 100644
index 14def2036..000000000
--- a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Threading;
-
-using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-
-namespace Dalamud.Game.Addon;
-
-/// Argument pool for Addon Lifecycle services.
-[ServiceManager.EarlyLoadedService]
-internal sealed class AddonLifecyclePooledArgs : IServiceType
-{
- private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
- private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
- private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
- private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
- private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
- private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
- private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
-
- [ServiceManager.ServiceConstructor]
- private AddonLifecyclePooledArgs()
- {
- }
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonRequestedUpdateArgs arg) =>
- new(out arg, this.addonRequestedUpdateArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonReceiveEventArgs arg) =>
- new(out arg, this.addonReceiveEventArgPool);
-
- /// Returns the object to the pool on dispose.
- /// The type.
- public readonly ref struct PooledEntry
- where T : AddonArgs, new()
- {
- private readonly Span pool;
- private readonly T obj;
-
- /// Initializes a new instance of the struct.
- /// An instance of the argument.
- /// The pool to rent from and return to.
- public PooledEntry(out T arg, Span pool)
- {
- this.pool = pool;
- foreach (ref var item in pool)
- {
- if (Interlocked.Exchange(ref item, null) is { } v)
- {
- this.obj = arg = v;
- return;
- }
- }
-
- this.obj = arg = new();
- }
-
- /// Returns the item to the pool.
- public void Dispose()
- {
- var tmp = this.obj;
- foreach (ref var item in this.pool)
- {
- if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
- return;
- tmp = tmp2;
- }
- }
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
index c008db08f..0b2ae1178 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Base class for AddonLifecycle AddonArgTypes.
///
-public abstract unsafe class AddonArgs
+public abstract class AddonArgs
{
///
/// Constant string representing the name of an addon that is invalid.
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
index 989e11912..7254ba7b3 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
@@ -3,7 +3,7 @@
///
/// Addon argument data for Draw events.
///
-public class AddonDrawArgs : AddonArgs, ICloneable
+public class AddonDrawArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -15,10 +15,4 @@ public class AddonDrawArgs : AddonArgs, ICloneable
///
public override AddonArgsType Type => AddonArgsType.Draw;
-
- ///
- public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
index d9401b414..12def3ad3 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for ReceiveEvent events.
///
-public class AddonFinalizeArgs : AddonArgs, ICloneable
+public class AddonFinalizeArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -15,10 +15,4 @@ public class AddonFinalizeArgs : AddonArgs, ICloneable
///
public override AddonArgsType Type => AddonArgsType.Finalize;
-
- ///
- public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
new file mode 100644
index 000000000..f3078af69
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
@@ -0,0 +1,18 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Draw events.
+///
+public class AddonGenericArgs : AddonArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonGenericArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Generic;
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
index 980fe4f2f..05f51b118 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for ReceiveEvent events.
///
-public class AddonReceiveEventArgs : AddonArgs, ICloneable
+public class AddonReceiveEventArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -36,19 +36,13 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
///
public nint Data { get; set; }
- ///
- public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.AtkEventType = default;
- this.EventParam = default;
- this.AtkEvent = default;
- this.Data = default;
+ this.AtkEventType = 0;
+ this.EventParam = 0;
+ this.AtkEvent = 0;
+ this.Data = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
index d28631c3c..c01c065c1 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Refresh events.
///
-public class AddonRefreshArgs : AddonArgs, ICloneable
+public class AddonRefreshArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -33,17 +33,11 @@ public class AddonRefreshArgs : AddonArgs, ICloneable
///
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
- ///
- public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.AtkValueCount = default;
- this.AtkValues = default;
+ this.AtkValueCount = 0;
+ this.AtkValues = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
index e87a980fd..bf00c5d6e 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for OnRequestedUpdate events.
///
-public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
+public class AddonRequestedUpdateArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -26,17 +26,11 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
///
public nint StringArrayData { get; set; }
- ///
- public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.NumberArrayData = default;
- this.StringArrayData = default;
+ this.NumberArrayData = 0;
+ this.StringArrayData = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
index 0dd9ecee2..9b7e86a61 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Setup events.
///
-public class AddonSetupArgs : AddonArgs, ICloneable
+public class AddonSetupArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -33,17 +33,11 @@ public class AddonSetupArgs : AddonArgs, ICloneable
///
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
- ///
- public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.AtkValueCount = default;
- this.AtkValues = default;
+ this.AtkValueCount = 0;
+ this.AtkValues = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
index a263f6ae4..bab62fc89 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Update events.
///
-public class AddonUpdateArgs : AddonArgs, ICloneable
+public class AddonUpdateArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -30,16 +30,10 @@ public class AddonUpdateArgs : AddonArgs, ICloneable
///
internal float TimeDeltaInternal { get; set; }
- ///
- public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.TimeDeltaInternal = default;
+ this.TimeDeltaInternal = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
index b58b5f4c7..95dc5f718 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
@@ -9,34 +9,39 @@ public enum AddonArgsType
/// Contains argument data for Setup.
///
Setup,
-
+
///
/// Contains argument data for Update.
///
Update,
-
+
///
/// Contains argument data for Draw.
- ///
+ ///
Draw,
-
+
///
/// Contains argument data for Finalize.
- ///
+ ///
Finalize,
-
+
///
/// Contains argument data for RequestedUpdate.
- ///
+ ///
RequestedUpdate,
-
+
///
/// Contains argument data for Refresh.
- ///
+ ///
Refresh,
-
+
///
/// Contains argument data for ReceiveEvent.
///
ReceiveEvent,
+
+ ///
+ /// Generic arg type that contains no meaningful data
+ ///
+ Generic,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
index 5fd0ac964..7738d6c6a 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
@@ -16,7 +16,7 @@ public enum AddonEvent
///
///
PreSetup,
-
+
///
/// An event that is fired after an addon has finished its initial setup. This event is particularly useful for
/// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data
@@ -64,7 +64,7 @@ public enum AddonEvent
///
///
PreFinalize,
-
+
///
/// An event that is fired before a call to is made in response to a
/// change in the subscribed or
@@ -81,13 +81,13 @@ public enum AddonEvent
/// to the Free Company's overview.
///
PreRequestedUpdate,
-
+
///
/// An event that is fired after an addon has finished processing an ArrayData update.
/// See for more information.
///
PostRequestedUpdate,
-
+
///
/// An event that is fired before an addon calls its method. Refreshes are
/// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to
@@ -96,13 +96,13 @@ public enum AddonEvent
///
///
PreRefresh,
-
+
///
/// An event that is fired after an addon has finished its refresh.
/// See for more information.
///
PostRefresh,
-
+
///
/// An event that is fired before an addon begins processing a user-driven event via
/// , such as mousing over an element or clicking a button. This event
@@ -112,10 +112,50 @@ public enum AddonEvent
///
///
PreReceiveEvent,
-
+
///
/// An event that is fired after an addon finishes calling its method.
/// See for more information.
///
PostReceiveEvent,
+
+ ///
+ /// An event that is fired before an addon processes its open method.
+ ///
+ PreOpen,
+
+ ///
+ /// An event that is fired after an addon has processed its open method.
+ ///
+ PostOpen,
+
+ ///
+ /// An even that is fired before an addon processes its close method.
+ ///
+ PreClose,
+
+ ///
+ /// An event that is fired after an addon has processed its close method.
+ ///
+ PostClose,
+
+ ///
+ /// An event that is fired before an addon processes its show method.
+ ///
+ PreShow,
+
+ ///
+ /// An event that is fired after an addon has processed its show method.
+ ///
+ PostShow,
+
+ ///
+ /// An event that is fired before an addon processes its hide method.
+ ///
+ PreHide,
+
+ ///
+ /// An event that is fired after an addon has processed its hide method.
+ ///
+ PostHide,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index b44ab8764..cea30d6be 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -1,16 +1,14 @@
using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics;
using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
-using Dalamud.Hooking.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
@@ -26,69 +24,33 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service.Get();
- [ServiceManager.ServiceDependency]
- private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
+ private readonly Dictionary modifiedTables = [];
- private readonly nint disallowedReceiveEventAddress;
-
- private readonly AddonLifecycleAddressResolver address;
- private readonly AddonSetupHook onAddonSetupHook;
- private readonly Hook onAddonFinalizeHook;
- private readonly CallHook onAddonDrawHook;
- private readonly CallHook onAddonUpdateHook;
- private readonly Hook onAddonRefreshHook;
- private readonly CallHook onAddonRequestedUpdateHook;
+ private Hook? onInitializeAddonHook;
[ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner)
{
- this.address = new AddonLifecycleAddressResolver();
- this.address.Setup(sigScanner);
+ this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
+ this.onInitializeAddonHook.Enable();
- this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent;
-
- var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon;
-
- this.onAddonSetupHook = new AddonSetupHook(this.address.AddonSetup, this.OnAddonSetup);
- this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
- this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw);
- this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate);
- this.onAddonRefreshHook = Hook.FromAddress(refreshAddonAddress, this.OnAddonRefresh);
- this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
-
- this.onAddonSetupHook.Enable();
- this.onAddonFinalizeHook.Enable();
- this.onAddonDrawHook.Enable();
- this.onAddonUpdateHook.Enable();
- this.onAddonRefreshHook.Enable();
- this.onAddonRequestedUpdateHook.Enable();
+ Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}");
}
- private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
-
- ///
- /// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
- ///
- internal List ReceiveEventListeners { get; } = new();
-
///
/// Gets a list of all AddonLifecycle Event Listeners.
///
- internal List EventListeners { get; } = new();
+ internal List EventListeners { get; } = [];
///
void IInternalDisposableService.DisposeService()
{
- this.onAddonSetupHook.Dispose();
- this.onAddonFinalizeHook.Dispose();
- this.onAddonDrawHook.Dispose();
- this.onAddonUpdateHook.Dispose();
- this.onAddonRefreshHook.Dispose();
- this.onAddonRequestedUpdateHook.Dispose();
+ this.onInitializeAddonHook?.Dispose();
+ this.onInitializeAddonHook = null;
- foreach (var receiveEventListener in this.ReceiveEventListeners)
+ foreach (var virtualTable in this.modifiedTables.Values)
{
- receiveEventListener.Dispose();
+ virtualTable.Dispose();
}
}
@@ -101,16 +63,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
this.framework.RunOnTick(() =>
{
this.EventListeners.Add(listener);
-
- // If we want receive event messages have an already active addon, enable the receive event hook.
- // If the addon isn't active yet, we'll grab the hook when it sets up.
- if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
- {
- if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
- {
- receiveEventListener.TryEnable();
- }
- }
});
}
@@ -122,24 +74,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{
// Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
listener.Removed = true;
-
+
this.framework.RunOnTick(() =>
{
this.EventListeners.Remove(listener);
-
- // If we are disabling an ReceiveEvent listener, check if we should disable the hook.
- if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
- {
- // Get the ReceiveEvent Listener for this addon
- if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
- {
- // If there are no other listeners listening for this event, disable the hook.
- if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
- {
- receiveEventListener.Disable();
- }
- }
- }
});
}
@@ -160,7 +98,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
// If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener.
if (listener.Removed)
continue;
-
+
// Match on string.empty for listeners that want events for all addons.
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
@@ -176,201 +114,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
}
- private void RegisterReceiveEventHook(AtkUnitBase* addon)
+ private void OnAddonInitialize(AtkUnitBase* addon)
{
- // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
- // Disallows hooking the core internal event handler.
- var addonName = addon->NameString;
- var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent;
- if (receiveEventAddress != this.disallowedReceiveEventAddress)
+ try
{
- // If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
- if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener)
+ this.LogInitialize(addon->NameString);
+
+ if (!this.modifiedTables.ContainsKey(addon->NameString))
{
- if (!existingListener.AddonNames.Contains(addonName))
+ // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
+ var managedVirtualTableEntry = new AddonVirtualTable(addon, this)
{
- existingListener.AddonNames.Add(addonName);
- }
- }
+ // This event is invoked when the game itself has disposed of an addon
+ // We can use this to know when to remove our virtual table entry
+ OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString),
+ };
- // Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
- else
- {
- this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
- }
-
- // If we have an active listener for this addon already, we need to activate this hook.
- if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
- {
- if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
- {
- receiveEventListener.TryEnable();
- }
+ this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry);
}
}
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
+ }
+
+ this.onInitializeAddonHook!.Original(addon);
}
- private void UnregisterReceiveEventHook(string addonName)
+ [Conditional("DEBUG")]
+ private void LogInitialize(string addonName)
{
- // Remove this addons ReceiveEvent Registration
- if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
- {
- eventListener.AddonNames.Remove(addonName);
-
- // If there are no more listeners let's remove and dispose.
- if (eventListener.AddonNames.Count is 0)
- {
- this.ReceiveEventListeners.Remove(eventListener);
- eventListener.Dispose();
- }
- }
- }
-
- private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
- {
- try
- {
- this.RegisterReceiveEventHook(addon);
- }
- catch (Exception e)
- {
- Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
- }
-
- using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.AtkValueCount = valueCount;
- arg.AtkValues = (nint)values;
- this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
- valueCount = arg.AtkValueCount;
- values = (AtkValue*)arg.AtkValues;
-
- try
- {
- addon->OnSetup(valueCount, values);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
- }
-
- private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
- {
- try
- {
- var addonName = atkUnitBase[0]->NameString;
- this.UnregisterReceiveEventHook(addonName);
- }
- catch (Exception e)
- {
- Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
- }
-
- using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
- arg.Clear();
- arg.Addon = (nint)atkUnitBase[0];
- this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
-
- try
- {
- this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
- }
- }
-
- private void OnAddonDraw(AtkUnitBase* addon)
- {
- using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
-
- try
- {
- addon->Draw();
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
- }
-
- private void OnAddonUpdate(AtkUnitBase* addon, float delta)
- {
- using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.TimeDeltaInternal = delta;
- this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
-
- try
- {
- addon->Update(delta);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
- }
-
- private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values)
- {
- var result = false;
-
- using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.AtkValueCount = valueCount;
- arg.AtkValues = (nint)values;
- this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
- valueCount = arg.AtkValueCount;
- values = (AtkValue*)arg.AtkValues;
-
- try
- {
- result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
- return result;
- }
-
- private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
- {
- using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.NumberArrayData = (nint)numberArrayData;
- arg.StringArrayData = (nint)stringArrayData;
- this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
- numberArrayData = (NumberArrayData**)arg.NumberArrayData;
- stringArrayData = (StringArrayData**)arg.StringArrayData;
-
- try
- {
- addon->OnRequestedUpdate(numberArrayData, stringArrayData);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
+ Log.Debug($"Initializing {addonName}");
}
}
@@ -387,7 +161,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service.Get();
- private readonly List eventListeners = new();
+ private readonly List eventListeners = [];
///
void IInternalDisposableService.DisposeService()
@@ -458,7 +232,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
this.eventListeners.RemoveAll(entry =>
{
if (entry.FunctionDelegate != handler) return false;
-
+
this.addonLifecycleService.UnregisterListener(entry);
return true;
});
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
index 854d666fd..1d767aac4 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
@@ -1,56 +1,24 @@
-using FFXIVClientStructs.FFXIV.Component.GUI;
+using Dalamud.Utility;
namespace Dalamud.Game.Addon.Lifecycle;
///
/// AddonLifecycleService memory address resolver.
///
-internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
+[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")]
+internal class AddonLifecycleAddressResolver : BaseAddressResolver
{
- ///
- /// Gets the address of the addon setup hook invoked by the AtkUnitManager.
- /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
- /// This is called for a majority of all addon OnSetup's.
- ///
- public nint AddonSetup { get; private set; }
-
- ///
- /// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
- /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
- /// This seems to be called rarely for specific addons.
- ///
- public nint AddonSetup2 { get; private set; }
-
///
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
///
public nint AddonFinalize { get; private set; }
- ///
- /// Gets the address of the addon draw hook invoked by virtual function call.
- ///
- public nint AddonDraw { get; private set; }
-
- ///
- /// Gets the address of the addon update hook invoked by virtual function call.
- ///
- public nint AddonUpdate { get; private set; }
-
- ///
- /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
- ///
- public nint AddonOnRequestedUpdate { get; private set; }
-
///
/// Scan for and setup any configured address pointers.
///
/// The signature scanner to facilitate setup.
protected override void Setup64Bit(ISigScanner sig)
{
- this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB");
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
- this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01");
- this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
- this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30");
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
deleted file mode 100644
index 0d2bcc7f2..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using System.Collections.Generic;
-
-using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-using Dalamud.Hooking;
-using Dalamud.Logging.Internal;
-
-using FFXIVClientStructs.FFXIV.Component.GUI;
-
-namespace Dalamud.Game.Addon.Lifecycle;
-
-///
-/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
-/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
-///
-internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
-{
- private static readonly ModuleLog Log = new("AddonLifecycle");
-
- [ServiceManager.ServiceDependency]
- private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// AddonLifecycle service instance.
- /// Initial Addon Requesting this listener.
- /// Address of Addon's ReceiveEvent function.
- internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
- {
- this.AddonLifecycle = service;
- this.AddonNames = [addonName];
- this.FunctionAddress = receiveEventAddress;
- }
-
- ///
- /// Gets the list of addons that use this receive event hook.
- ///
- public List AddonNames { get; init; }
-
- ///
- /// Gets the address of the ReceiveEvent function as provided by the vtable on setup.
- ///
- public nint FunctionAddress { get; init; }
-
- ///
- /// Gets the contained hook for these addons.
- ///
- public Hook? Hook { get; private set; }
-
- ///
- /// Gets or sets the Reference to AddonLifecycle service instance.
- ///
- private AddonLifecycle AddonLifecycle { get; set; }
-
- ///
- /// Try to hook and enable this receive event handler.
- ///
- public void TryEnable()
- {
- this.Hook ??= Hook.FromAddress(this.FunctionAddress, this.OnReceiveEvent);
- this.Hook?.Enable();
- }
-
- ///
- /// Disable the hook for this receive event handler.
- ///
- public void Disable()
- {
- this.Hook?.Disable();
- }
-
- ///
- public void Dispose()
- {
- this.Hook?.Dispose();
- }
-
- private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
- {
- // Check that we didn't get here through a call to another addons handler.
- var addonName = addon->NameString;
- if (!this.AddonNames.Contains(addonName))
- {
- this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
- return;
- }
-
- using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
- arg.Clear();
- arg.Addon = (nint)addon;
- arg.AtkEventType = (byte)eventType;
- arg.EventParam = eventParam;
- arg.AtkEvent = (IntPtr)atkEvent;
- arg.Data = (nint)atkEventData;
- this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
- eventType = (AtkEventType)arg.AtkEventType;
- eventParam = arg.EventParam;
- atkEvent = (AtkEvent*)arg.AtkEvent;
- atkEventData = (AtkEventData*)arg.Data;
-
- try
- {
- this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
- }
-
- this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs
deleted file mode 100644
index 297323b8f..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System.Runtime.InteropServices;
-
-using Reloaded.Hooks.Definitions;
-
-namespace Dalamud.Game.Addon.Lifecycle;
-
-///
-/// This class represents a callsite hook used to replace the address of the OnSetup function in r9.
-///
-/// Delegate signature for this hook.
-internal class AddonSetupHook : IDisposable where T : Delegate
-{
- private readonly Reloaded.Hooks.AsmHook asmHook;
-
- private T? detour;
- private bool activated;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Address of the instruction to replace.
- /// Delegate to invoke.
- internal AddonSetupHook(nint address, T detour)
- {
- this.detour = detour;
-
- var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
- var code = new[]
- {
- "use64",
- $"mov r9, 0x{detourPtr:X8}",
- };
-
- var opt = new AsmHookOptions
- {
- PreferRelativeJump = true,
- Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
- MaxOpcodeSize = 5,
- };
-
- this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
- }
-
- ///
- /// Gets a value indicating whether the hook is enabled.
- ///
- public bool IsEnabled => this.asmHook.IsEnabled;
-
- ///
- /// Starts intercepting a call to the function.
- ///
- public void Enable()
- {
- if (!this.activated)
- {
- this.activated = true;
- this.asmHook.Activate();
- return;
- }
-
- this.asmHook.Enable();
- }
-
- ///
- /// Stops intercepting a call to the function.
- ///
- public void Disable()
- {
- this.asmHook.Disable();
- }
-
- ///
- /// Remove a hook from the current process.
- ///
- public void Dispose()
- {
- this.asmHook.Disable();
- this.detour = null;
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
new file mode 100644
index 000000000..58e32a252
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -0,0 +1,405 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Logging.Internal;
+
+using FFXIVClientStructs.FFXIV.Client.System.Memory;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Lifecycle;
+
+///
+/// Represents a class that holds references to an addons original and modified virtual table entries.
+///
+internal unsafe class AddonVirtualTable : IDisposable
+{
+ // This need to be at minimum the largest virtual table size of all addons
+ // Copying extra entries is not problematic, and is considered safe.
+ private const int VirtualTableEntryCount = 200;
+
+ private const bool EnableAdvancedLogging = true;
+ private const bool EnableSpammyLogging = false;
+
+ private static readonly ModuleLog Log = new("LifecycleVT");
+
+ private readonly AddonLifecycle lifecycleService;
+
+ // Obsolete warning is only to prevent users from creating their own event objects.
+#pragma warning disable CS0618 // Type or member is obsolete
+ private readonly AddonSetupArgs addonSetupArg = new();
+ private readonly AddonFinalizeArgs addonFinalizeArg = new();
+ private readonly AddonDrawArgs addonDrawArg = new();
+ private readonly AddonUpdateArgs addonUpdateArg = new();
+ private readonly AddonRefreshArgs addonRefreshArg = new();
+ private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new();
+ private readonly AddonReceiveEventArgs addonReceiveEventArg = new();
+ private readonly AddonGenericArgs addonGenericArg = new();
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ private readonly AtkUnitBase* atkUnitBase;
+
+ private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable;
+ private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable;
+
+ // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
+ // the CLR needs to know they are in use, or it will invalidate them causing random crashing.
+ private readonly AtkUnitBase.Delegates.Dtor destructorFunction;
+ private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction;
+ private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction;
+ private readonly AtkUnitBase.Delegates.Draw drawFunction;
+ private readonly AtkUnitBase.Delegates.Update updateFunction;
+ private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction;
+ private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction;
+ private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction;
+ private readonly AtkUnitBase.Delegates.Open openFunction;
+ private readonly AtkUnitBase.Delegates.Close closeFunction;
+ private readonly AtkUnitBase.Delegates.Show showFunction;
+ private readonly AtkUnitBase.Delegates.Hide hideFunction;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// AtkUnitBase* for the addon to replace the table of.
+ /// Reference to AddonLifecycle service to callback and invoke listeners.
+ internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService)
+ {
+ this.atkUnitBase = addon;
+ this.lifecycleService = lifecycleService;
+
+ // Save original virtual table
+ this.originalVirtualTable = addon->VirtualTable;
+
+ // Create copy of original table
+ // Note this will copy any derived/overriden functions that this specific addon has.
+ // Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
+ this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
+ NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
+
+ // Overwrite the addons existing virtual table with our own
+ addon->VirtualTable = this.modifiedVirtualTable;
+
+ // Pin each of our listener functions
+ this.destructorFunction = this.OnAddonDestructor;
+ this.onSetupFunction = this.OnAddonSetup;
+ this.finalizerFunction = this.OnAddonFinalize;
+ this.drawFunction = this.OnAddonDraw;
+ this.updateFunction = this.OnAddonUpdate;
+ this.onRefreshFunction = this.OnAddonRefresh;
+ this.onRequestedUpdateFunction = this.OnRequestedUpdate;
+ this.onReceiveEventFunction = this.OnAddonReceiveEvent;
+ this.openFunction = this.OnAddonOpen;
+ this.closeFunction = this.OnAddonClose;
+ this.showFunction = this.OnAddonShow;
+ this.hideFunction = this.OnAddonHide;
+
+ // Overwrite specific virtual table entries
+ this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
+ this.modifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
+ this.modifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
+ this.modifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
+ this.modifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
+ this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
+ this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
+ this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
+ this.modifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction);
+ this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
+ this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction);
+ this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
+ }
+
+ ///
+ /// Gets an event that is invoked when this addon's Finalize method is called from native.
+ ///
+ public required Action OnAddonFinalized { get; init; }
+
+ ///
+ /// WARNING! This should not be called at any time except during dalamud unload.
+ ///
+ public void Dispose()
+ {
+ this.atkUnitBase->VirtualTable = this.originalVirtualTable;
+ IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
+ }
+
+ private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
+ {
+ this.LogEvent();
+
+ var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags);
+
+ if ((freeFlags & 1) == 1)
+ {
+ IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
+ this.OnAddonFinalized();
+ }
+
+ return result;
+ }
+
+ private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
+ {
+ this.LogEvent();
+
+ this.addonSetupArg.Clear();
+ this.addonSetupArg.Addon = addon;
+ this.addonSetupArg.AtkValueCount = valueCount;
+ this.addonSetupArg.AtkValues = (nint)values;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg);
+ valueCount = this.addonSetupArg.AtkValueCount;
+ values = (AtkValue*)this.addonSetupArg.AtkValues;
+
+ try
+ {
+ this.originalVirtualTable->OnSetup(addon, valueCount, values);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg);
+ }
+
+ private void OnAddonFinalize(AtkUnitBase* thisPtr)
+ {
+ this.LogEvent();
+
+ this.addonFinalizeArg.Clear();
+ this.addonFinalizeArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg);
+
+ try
+ {
+ this.originalVirtualTable->Finalizer(thisPtr);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
+ }
+ }
+
+ private void OnAddonDraw(AtkUnitBase* addon)
+ {
+ this.LogEvent();
+
+ this.addonDrawArg.Clear();
+ this.addonDrawArg.Addon = addon;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg);
+
+ try
+ {
+ this.originalVirtualTable->Draw(addon);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg);
+ }
+
+ private void OnAddonUpdate(AtkUnitBase* addon, float delta)
+ {
+ this.LogEvent();
+
+ this.addonUpdateArg.Clear();
+ this.addonUpdateArg.Addon = addon;
+ this.addonUpdateArg.TimeDeltaInternal = delta;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg);
+
+ try
+ {
+ this.originalVirtualTable->Update(addon, delta);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg);
+ }
+
+ private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
+ {
+ this.LogEvent();
+
+ var result = false;
+
+ this.addonRefreshArg.Clear();
+ this.addonRefreshArg.Addon = addon;
+ this.addonRefreshArg.AtkValueCount = valueCount;
+ this.addonRefreshArg.AtkValues = (nint)values;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg);
+ valueCount = this.addonRefreshArg.AtkValueCount;
+ values = (AtkValue*)this.addonRefreshArg.AtkValues;
+
+ try
+ {
+ result = this.originalVirtualTable->OnRefresh(addon, valueCount, values);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg);
+ return result;
+ }
+
+ private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
+ {
+ this.LogEvent();
+
+ this.addonRequestedUpdateArg.Clear();
+ this.addonRequestedUpdateArg.Addon = addon;
+ this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData;
+ this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg);
+ numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData;
+ stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData;
+
+ try
+ {
+ this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg);
+ }
+
+ private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
+ {
+ this.LogEvent();
+
+ this.addonReceiveEventArg.Clear();
+ this.addonReceiveEventArg.Addon = (nint)addon;
+ this.addonReceiveEventArg.AtkEventType = (byte)eventType;
+ this.addonReceiveEventArg.EventParam = eventParam;
+ this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent;
+ this.addonReceiveEventArg.Data = (nint)atkEventData;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg);
+ eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType;
+ eventParam = this.addonReceiveEventArg.EventParam;
+ atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent;
+ atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data;
+
+ try
+ {
+ this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg);
+ }
+
+ private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
+ {
+ this.LogEvent();
+
+ var result = false;
+
+ this.addonGenericArg.Clear();
+ this.addonGenericArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg);
+
+ try
+ {
+ result = this.originalVirtualTable->Open(thisPtr, depthLayer);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg);
+
+ return result;
+ }
+
+ private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
+ {
+ this.LogEvent();
+
+ var result = false;
+
+ this.addonGenericArg.Clear();
+ this.addonGenericArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg);
+
+ try
+ {
+ result = this.originalVirtualTable->Close(thisPtr, fireCallback);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg);
+
+ return result;
+ }
+
+ private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags)
+ {
+ this.LogEvent();
+
+ this.addonGenericArg.Clear();
+ this.addonGenericArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg);
+
+ try
+ {
+ this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg);
+ }
+
+ private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
+ {
+ this.LogEvent();
+
+ this.addonGenericArg.Clear();
+ this.addonGenericArg.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg);
+
+ try
+ {
+ this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg);
+ }
+
+ [Conditional("DEBUG")]
+ private void LogEvent([CallerMemberName] string caller = "")
+ {
+ if (EnableAdvancedLogging)
+ {
+ if (!EnableSpammyLogging)
+ {
+ if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
+ return;
+ }
+
+ Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}");
+ }
+ }
+}
diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs
deleted file mode 100644
index 92bc6e31a..000000000
--- a/Dalamud/Hooking/Internal/CallHook.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using System.Runtime.InteropServices;
-
-using Reloaded.Hooks.Definitions;
-
-namespace Dalamud.Hooking.Internal;
-
-///
-/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook.
-/// This is a destructive operation, no other callsite hooks can coexist at the same address.
-///
-/// There's no .Original for this hook type.
-/// This is only intended for be for functions where the parameters provided allow you to invoke the original call.
-///
-/// This class was specifically added for hooking virtual function callsites.
-/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered.
-///
-/// Delegate signature for this hook.
-internal class CallHook : IDalamudHook where T : Delegate
-{
- private readonly Reloaded.Hooks.AsmHook asmHook;
-
- private T? detour;
- private bool activated;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Address of the instruction to replace.
- /// Delegate to invoke.
- internal CallHook(nint address, T detour)
- {
- ArgumentNullException.ThrowIfNull(detour);
-
- this.detour = detour;
- this.Address = address;
-
- var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
- var code = new[]
- {
- "use64",
- $"mov rax, 0x{detourPtr:X8}",
- "call rax",
- };
-
- var opt = new AsmHookOptions
- {
- PreferRelativeJump = true,
- Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
- MaxOpcodeSize = 5,
- };
-
- this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
- }
-
- ///
- /// Gets a value indicating whether the hook is enabled.
- ///
- public bool IsEnabled => this.asmHook.IsEnabled;
-
- ///
- public IntPtr Address { get; }
-
- ///
- public string BackendName => "Reloaded AsmHook";
-
- ///
- public bool IsDisposed => this.detour == null;
-
- ///
- /// Starts intercepting a call to the function.
- ///
- public void Enable()
- {
- if (!this.activated)
- {
- this.activated = true;
- this.asmHook.Activate();
- return;
- }
-
- this.asmHook.Enable();
- }
-
- ///
- /// Stops intercepting a call to the function.
- ///
- public void Disable()
- {
- this.asmHook.Disable();
- }
-
- ///
- /// Remove a hook from the current process.
- ///
- public void Dispose()
- {
- this.asmHook.Disable();
- this.detour = null;
- }
-}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
index b58166e89..c336f895e 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -1,10 +1,8 @@
-using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle;
-using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@@ -54,13 +52,6 @@ public class AddonLifecycleWidget : IDataWindowWidget
this.DrawEventListeners();
ImGui.Unindent();
}
-
- if (ImGui.CollapsingHeader("ReceiveEvent Hooks"u8))
- {
- ImGui.Indent();
- this.DrawReceiveEventHooks();
- ImGui.Unindent();
- }
}
private void DrawEventListeners()
@@ -100,46 +91,4 @@ public class AddonLifecycleWidget : IDataWindowWidget
}
}
}
-
- private void DrawReceiveEventHooks()
- {
- if (!this.Ready) return;
-
- var listeners = this.AddonLifecycle.ReceiveEventListeners;
-
- if (listeners.Count == 0)
- {
- ImGui.Text("No ReceiveEvent Hooks are Registered"u8);
- }
-
- foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners)
- {
- if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames)))
- {
- ImGui.Columns(2);
-
- var functionAddress = receiveEventListener.FunctionAddress;
-
- ImGui.Text("Hook Address"u8);
- ImGui.NextColumn();
- ImGui.Text($"0x{functionAddress:X} (ffxiv_dx11.exe+{functionAddress - Process.GetCurrentProcess().MainModule!.BaseAddress:X})");
-
- ImGui.NextColumn();
- ImGui.Text("Hook Status"u8);
- ImGui.NextColumn();
- if (receiveEventListener.Hook is null)
- {
- ImGui.Text("Hook is null"u8);
- }
- else
- {
- var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
- var text = receiveEventListener.Hook.IsEnabled ? "Enabled"u8 : "Disabled"u8;
- ImGui.TextColored(color, text);
- }
-
- ImGui.Columns(1);
- }
- }
- }
}
From 27a7adfdb9851173835fb09ec2af0141d4a329e5 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Tue, 25 Nov 2025 18:56:34 -0800
Subject: [PATCH 061/201] Minor cleanup
---
Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 2 +-
Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 11 ++++++-----
.../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +-
Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +-
4 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
index 95dc5f718..de32bd254 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
@@ -41,7 +41,7 @@ public enum AddonArgsType
ReceiveEvent,
///
- /// Generic arg type that contains no meaningful data
+ /// Generic arg type that contains no meaningful data.
///
Generic,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index cea30d6be..0c23f5661 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -33,8 +33,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{
this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
this.onInitializeAddonHook.Enable();
-
- Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}");
}
///
@@ -48,10 +46,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
this.onInitializeAddonHook?.Dispose();
this.onInitializeAddonHook = null;
- foreach (var virtualTable in this.modifiedTables.Values)
+ this.framework.RunOnFrameworkThread(() =>
{
- virtualTable.Dispose();
- }
+ foreach (var virtualTable in this.modifiedTables.Values)
+ {
+ virtualTable.Dispose();
+ }
+ });
}
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
index 1d767aac4..9359870a5 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
///
/// AddonLifecycleService memory address resolver.
///
-[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")]
+[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")]
internal class AddonLifecycleAddressResolver : BaseAddressResolver
{
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index 58e32a252..ca5d970ef 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -19,7 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 200;
- private const bool EnableAdvancedLogging = true;
+ private const bool EnableAdvancedLogging = false;
private const bool EnableSpammyLogging = false;
private static readonly ModuleLog Log = new("LifecycleVT");
From 0533872a73f2caccc9ce6f6563070554bf7de59f Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Tue, 25 Nov 2025 20:45:54 -0800
Subject: [PATCH 062/201] Fix unreachable code complaint
---
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 39 +++++++++----------
1 file changed, 18 insertions(+), 21 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index ca5d970ef..54c91248e 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -19,8 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 200;
- private const bool EnableAdvancedLogging = false;
- private const bool EnableSpammyLogging = false;
+ private const bool EnableLogging = false;
private static readonly ModuleLog Log = new("LifecycleVT");
@@ -125,7 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags);
@@ -140,7 +139,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonSetupArg.Clear();
this.addonSetupArg.Addon = addon;
@@ -164,7 +163,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonFinalize(AtkUnitBase* thisPtr)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonFinalizeArg.Clear();
this.addonFinalizeArg.Addon = thisPtr;
@@ -182,7 +181,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonDraw(AtkUnitBase* addon)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonDrawArg.Clear();
this.addonDrawArg.Addon = addon;
@@ -202,7 +201,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonUpdateArg.Clear();
this.addonUpdateArg.Addon = addon;
@@ -223,7 +222,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
var result = false;
@@ -250,7 +249,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonRequestedUpdateArg.Clear();
this.addonRequestedUpdateArg.Addon = addon;
@@ -274,7 +273,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonReceiveEventArg.Clear();
this.addonReceiveEventArg.Addon = (nint)addon;
@@ -302,7 +301,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
var result = false;
@@ -326,7 +325,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
var result = false;
@@ -350,7 +349,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
@@ -370,7 +369,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
{
- this.LogEvent();
+ this.LogEvent(EnableLogging);
this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
@@ -389,15 +388,13 @@ internal unsafe class AddonVirtualTable : IDisposable
}
[Conditional("DEBUG")]
- private void LogEvent([CallerMemberName] string caller = "")
+ private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
{
- if (EnableAdvancedLogging)
+ if (loggingEnabled)
{
- if (!EnableSpammyLogging)
- {
- if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
- return;
- }
+ // Manually disable the really spammy log events, you can comment this out if you need to debug them.
+ if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
+ return;
Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}");
}
From 4f59e0951303b2d4eca5700478982b93d2ca7ab6 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Thu, 27 Nov 2025 14:24:35 -0800
Subject: [PATCH 063/201] Improve LifecycleInvoke efficiency with Dictionary
---
.../Game/Addon/Lifecycle/AddonLifecycle.cs | 29 +++++++------------
1 file changed, 10 insertions(+), 19 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index 0c23f5661..cf1270803 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
///
/// Gets a list of all AddonLifecycle Event Listeners.
///
- internal List EventListeners { get; } = [];
+ internal Dictionary> EventListeners { get; } = [];
///
void IInternalDisposableService.DisposeService()
@@ -61,10 +61,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to register.
internal void RegisterListener(AddonLifecycleEventListener listener)
{
- this.framework.RunOnTick(() =>
- {
- this.EventListeners.Add(listener);
- });
+ this.EventListeners.TryAdd(listener.EventType, [ listener ]);
+ this.EventListeners[listener.EventType].Add(listener);
}
///
@@ -73,13 +71,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to unregister.
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
- // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
- listener.Removed = true;
-
- this.framework.RunOnTick(() =>
+ if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList))
{
- this.EventListeners.Remove(listener);
- });
+ listenerList.Remove(listener);
+ }
}
///
@@ -90,16 +85,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// What to blame on errors.
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
+ // Early return if we don't have any listeners of this type
+ if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return;
+
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
- foreach (var listener in this.EventListeners)
+ foreach (var listener in listenerList)
{
- if (listener.EventType != eventType)
- continue;
-
- // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener.
- if (listener.Removed)
- continue;
-
// Match on string.empty for listeners that want events for all addons.
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
From b82b4f40cec89a9c5212c9ec1ba8ac8143352e18 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Thu, 27 Nov 2025 14:30:40 -0800
Subject: [PATCH 064/201] Use hashset to prevent duplicate entries
---
Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index cf1270803..403671920 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
///
/// Gets a list of all AddonLifecycle Event Listeners.
///
- internal Dictionary> EventListeners { get; } = [];
+ internal Dictionary> EventListeners { get; } = [];
///
void IInternalDisposableService.DisposeService()
From c3e3e4aa8582e83c8efb74b991c59714111411ad Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 28 Nov 2025 08:35:54 -0800
Subject: [PATCH 065/201] Fix accidentally breaking widget
---
.../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
index c336f895e..73c4e540a 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle;
@@ -58,12 +57,11 @@ public class AddonLifecycleWidget : IDataWindowWidget
{
if (!this.Ready) return;
- foreach (var eventType in Enum.GetValues())
+ foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners)
{
- if (ImGui.CollapsingHeader(eventType.ToString()))
+ if (ImGui.CollapsingHeader(listenerType.ToString()))
{
ImGui.Indent();
- var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList();
if (listeners.Count == 0)
{
From f8725e5f37e4a9f6a620eaf328da147124c7e8a9 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 28 Nov 2025 09:08:24 -0800
Subject: [PATCH 066/201] further improve performance
---
.../Game/Addon/Lifecycle/AddonLifecycle.cs | 61 ++++++++++++++-----
.../Data/Widgets/AddonLifecycleWidget.cs | 40 ++++++------
2 files changed, 67 insertions(+), 34 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index 403671920..e38f56921 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -38,7 +38,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
///
/// Gets a list of all AddonLifecycle Event Listeners.
///
- internal Dictionary> EventListeners { get; } = [];
+ /// Mapping is: EventType -> AddonName -> ListenerList
+ internal Dictionary>> EventListeners { get; } = [];
///
void IInternalDisposableService.DisposeService()
@@ -61,8 +62,18 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to register.
internal void RegisterListener(AddonLifecycleEventListener listener)
{
- this.EventListeners.TryAdd(listener.EventType, [ listener ]);
- this.EventListeners[listener.EventType].Add(listener);
+ if (!this.EventListeners.ContainsKey(listener.EventType))
+ {
+ this.EventListeners.TryAdd(listener.EventType, []);
+ }
+
+ // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
+ if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
+ {
+ this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []);
+ }
+
+ this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
}
///
@@ -71,9 +82,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to unregister.
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
- if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList))
+ if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
{
- listenerList.Remove(listener);
+ if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
+ {
+ addonListener.Remove(listener);
+ }
}
}
@@ -86,22 +100,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
// Early return if we don't have any listeners of this type
- if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return;
+ if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
- // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
- foreach (var listener in listenerList)
+ // Handle listeners for this event type that don't care which addon is triggering it
+ if (addonListeners.TryGetValue(string.Empty, out var globalListeners))
{
- // Match on string.empty for listeners that want events for all addons.
- if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
- continue;
-
- try
+ foreach (var listener in globalListeners)
{
- listener.FunctionDelegate.Invoke(eventType, args);
+ try
+ {
+ listener.FunctionDelegate.Invoke(eventType, args);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
+ }
}
- catch (Exception e)
+ }
+
+ // Handle listeners that are listening for this addon and event type specifically
+ if (addonListeners.TryGetValue(args.AddonName, out var addonListener))
+ {
+ foreach (var listener in addonListener)
{
- Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
+ try
+ {
+ listener.FunctionDelegate.Invoke(eventType, args);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
+ }
}
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
index 73c4e540a..0f193556b 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -2,7 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle;
-using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Utility;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@@ -57,35 +58,38 @@ public class AddonLifecycleWidget : IDataWindowWidget
{
if (!this.Ready) return;
- foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners)
+ foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners)
{
- if (ImGui.CollapsingHeader(listenerType.ToString()))
+ using var eventId = ImRaii.PushId(eventType.ToString());
+
+ if (ImGui.CollapsingHeader(eventType.ToString()))
{
- ImGui.Indent();
+ using var eventIndent = ImRaii.PushIndent();
- if (listeners.Count == 0)
+ if (addonListeners.Count == 0)
{
- ImGui.Text("No Listeners Registered for Event"u8);
+ ImGui.Text("No Addons Registered for Event"u8);
}
- if (ImGui.BeginTable("AddonLifecycleListenersTable"u8, 2))
+ foreach (var (addonName, listeners) in addonListeners)
{
- ImGui.TableSetupColumn("##AddonName"u8, ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale);
- ImGui.TableSetupColumn("##MethodInvoke"u8, ImGuiTableColumnFlags.WidthStretch);
+ using var addonId = ImRaii.PushId(addonName);
- foreach (var listener in listeners)
+ if (ImGui.CollapsingHeader(addonName.IsNullOrEmpty() ? "GLOBAL" : addonName))
{
- ImGui.TableNextColumn();
- ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName);
+ using var addonIndent = ImRaii.PushIndent();
- ImGui.TableNextColumn();
- ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}");
+ if (listeners.Count == 0)
+ {
+ ImGui.Text("No Listeners Registered for Event"u8);
+ }
+
+ foreach (var listener in listeners)
+ {
+ ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}");
+ }
}
-
- ImGui.EndTable();
}
-
- ImGui.Unindent();
}
}
}
From e01acb4a80727def0dd77b2e72202404b139f099 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 28 Nov 2025 09:11:13 -0800
Subject: [PATCH 067/201] Remove redundant header
---
.../Windows/Data/Widgets/AddonLifecycleWidget.cs | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
index 0f193556b..4fb13b81a 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -46,18 +46,6 @@ public class AddonLifecycleWidget : IDataWindowWidget
return;
}
- if (ImGui.CollapsingHeader("Listeners"u8))
- {
- ImGui.Indent();
- this.DrawEventListeners();
- ImGui.Unindent();
- }
- }
-
- private void DrawEventListeners()
- {
- if (!this.Ready) return;
-
foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners)
{
using var eventId = ImRaii.PushId(eventType.ToString());
From be3f71dc734c13ed4ad222ce46fcff7338bb0f82 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 28 Nov 2025 09:44:35 -0800
Subject: [PATCH 068/201] Fix copy paste error
---
Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +-
Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index e38f56921..d3d0fcebe 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -37,7 +37,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
///
/// Gets a list of all AddonLifecycle Event Listeners.
- ///
+ ///
/// Mapping is: EventType -> AddonName -> ListenerList
internal Dictionary>> EventListeners { get; } = [];
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index 54c91248e..db698e626 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -167,7 +167,7 @@ internal unsafe class AddonVirtualTable : IDisposable
this.addonFinalizeArg.Clear();
this.addonFinalizeArg.Addon = thisPtr;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg);
try
{
From eb9555ee22c3d240a7b770f985207d172c6c8284 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 10:08:40 -0800
Subject: [PATCH 069/201] Better unload
---
.../Game/Addon/Lifecycle/AddonLifecycle.cs | 33 +++++--------------
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 15 +++------
2 files changed, 14 insertions(+), 34 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index d3d0fcebe..5d121bea4 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -19,13 +19,13 @@ namespace Dalamud.Game.Addon.Lifecycle;
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IInternalDisposableService
{
+ ///
+ /// Gets a list of all allocated addon virtual tables.
+ ///
+ public static readonly List AllocatedTables = [];
+
private static readonly ModuleLog Log = new("AddonLifecycle");
- [ServiceManager.ServiceDependency]
- private readonly Framework framework = Service.Get();
-
- private readonly Dictionary modifiedTables = [];
-
private Hook? onInitializeAddonHook;
[ServiceManager.ServiceConstructor]
@@ -47,13 +47,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
this.onInitializeAddonHook?.Dispose();
this.onInitializeAddonHook = null;
- this.framework.RunOnFrameworkThread(() =>
- {
- foreach (var virtualTable in this.modifiedTables.Values)
- {
- virtualTable.Dispose();
- }
- });
+ AllocatedTables.ForEach(entry => entry.Dispose());
+ AllocatedTables.Clear();
}
///
@@ -141,18 +136,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{
this.LogInitialize(addon->NameString);
- if (!this.modifiedTables.ContainsKey(addon->NameString))
- {
- // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
- var managedVirtualTableEntry = new AddonVirtualTable(addon, this)
- {
- // This event is invoked when the game itself has disposed of an addon
- // We can use this to know when to remove our virtual table entry
- OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString),
- };
-
- this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry);
- }
+ // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
+ AllocatedTables.Add(new AddonVirtualTable(addon, this));
}
catch (Exception e)
{
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index db698e626..d91cd648f 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Logging.Internal;
@@ -108,17 +109,11 @@ internal unsafe class AddonVirtualTable : IDisposable
this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
}
- ///
- /// Gets an event that is invoked when this addon's Finalize method is called from native.
- ///
- public required Action OnAddonFinalized { get; init; }
-
- ///
- /// WARNING! This should not be called at any time except during dalamud unload.
- ///
+ ///
public void Dispose()
{
- this.atkUnitBase->VirtualTable = this.originalVirtualTable;
+ // Ensure restoration is done atomically.
+ Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable);
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
}
@@ -131,7 +126,7 @@ internal unsafe class AddonVirtualTable : IDisposable
if ((freeFlags & 1) == 1)
{
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
- this.OnAddonFinalized();
+ AddonLifecycle.AllocatedTables.Remove(this);
}
return result;
From 08c176828639fefeae28e74030c34112d963cba8 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 10:39:35 -0800
Subject: [PATCH 070/201] Bunch of stuff...
---
Dalamud/Configuration/PluginConfigurations.cs | 2 +-
.../Lifecycle/AddonArgTypes/AddonArgs.cs | 31 ++-----------------
.../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 9 ++++--
.../AddonArgTypes/AddonFinalizeArgs.cs | 7 +++--
.../AddonArgTypes/AddonGenericArgs.cs | 3 +-
.../AddonArgTypes/AddonReceiveEventArgs.cs | 18 +++--------
.../AddonArgTypes/AddonRefreshArgs.cs | 15 +++------
.../AddonArgTypes/AddonRequestedUpdateArgs.cs | 11 +------
.../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 15 +++------
.../AddonArgTypes/AddonUpdateArgs.cs | 26 +++++++---------
.../AddonLifecycleAddressResolver.cs | 2 +-
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 14 ---------
Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +-
Dalamud/Interface/Animation/Easing.cs | 2 +-
...ToDoAttribute.cs => Api14ToDoAttribute.cs} | 6 ++--
Dalamud/Utility/Api15ToDoAttribute.cs | 25 +++++++++++++++
Dalamud/Utility/Util.cs | 2 +-
17 files changed, 74 insertions(+), 116 deletions(-)
rename Dalamud/Utility/{Api13ToDoAttribute.cs => Api14ToDoAttribute.cs} (75%)
create mode 100644 Dalamud/Utility/Api15ToDoAttribute.cs
diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs
index fa2969d31..c01ab2af0 100644
--- a/Dalamud/Configuration/PluginConfigurations.cs
+++ b/Dalamud/Configuration/PluginConfigurations.cs
@@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
///
/// Configuration to store settings for a dalamud plugin.
///
-[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
+[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
public sealed class PluginConfigurations
{
private readonly DirectoryInfo configDirectory;
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
index 0b2ae1178..62ca47238 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
@@ -33,41 +33,14 @@ public abstract class AddonArgs
///
public abstract AddonArgsType Type { get; }
- ///
- /// Checks if addon name matches the given span of char.
- ///
- /// The name to check.
- /// Whether it is the case.
- internal bool IsAddon(string name)
- {
- if (this.Addon.IsNull)
- return false;
-
- if (name.Length is 0 or > 32)
- return false;
-
- if (string.IsNullOrEmpty(this.Addon.Name))
- return false;
-
- return name == this.Addon.Name;
- }
-
- ///
- /// Clears this AddonArgs values.
- ///
- internal virtual void Clear()
- {
- this.addonName = null;
- this.Addon = 0;
- }
-
///
/// Helper method for ensuring the name of the addon is valid.
///
/// The name of the addon for this object. when invalid.
private string GetAddonName()
{
- if (this.Addon.IsNull) return InvalidAddon;
+ if (this.Addon.IsNull)
+ return InvalidAddon;
var name = this.Addon.Name;
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
index 7254ba7b3..a834d2983 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
@@ -1,15 +1,18 @@
-namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Utility;
+
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Draw events.
///
+[Obsolete("Use AddonGenericArgs instead.")]
+[Api15ToDo("Remove this")]
public class AddonDrawArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonDrawArgs()
+ internal AddonDrawArgs()
{
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
index 12def3ad3..11d15a081 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
@@ -1,15 +1,18 @@
+using Dalamud.Utility;
+
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for ReceiveEvent events.
///
+[Obsolete("Use AddonGenericArgs instead.")]
+[Api15ToDo("Remove this")]
public class AddonFinalizeArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonFinalizeArgs()
+ internal AddonFinalizeArgs()
{
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
index f3078af69..a20e9d23b 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
@@ -8,8 +8,7 @@ public class AddonGenericArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonGenericArgs()
+ internal AddonGenericArgs()
{
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
index 05f51b118..bb8168075 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
@@ -1,3 +1,5 @@
+using Dalamud.Utility;
+
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
@@ -8,8 +10,7 @@ public class AddonReceiveEventArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonReceiveEventArgs()
+ internal AddonReceiveEventArgs()
{
}
@@ -32,17 +33,8 @@ public class AddonReceiveEventArgs : AddonArgs
public nint AtkEvent { get; set; }
///
- /// Gets or sets the pointer to a block of data for this event message.
+ /// Gets or sets the pointer to an AtkEventData for this event message.
///
+ [Api14ToDo("Rename to AtkEventData")]
public nint Data { get; set; }
-
- ///
- internal override void Clear()
- {
- base.Clear();
- this.AtkEventType = 0;
- this.EventParam = 0;
- this.AtkEvent = 0;
- this.Data = 0;
- }
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
index c01c065c1..8af017318 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
@@ -1,3 +1,5 @@
+using Dalamud.Utility;
+
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@@ -10,8 +12,7 @@ public class AddonRefreshArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonRefreshArgs()
+ internal AddonRefreshArgs()
{
}
@@ -31,13 +32,7 @@ public class AddonRefreshArgs : AddonArgs
///
/// Gets the AtkValues in the form of a span.
///
+ [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")]
+ [Api15ToDo("Remove this")]
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
-
- ///
- internal override void Clear()
- {
- base.Clear();
- this.AtkValueCount = 0;
- this.AtkValues = 0;
- }
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
index bf00c5d6e..7005b77c2 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
@@ -8,8 +8,7 @@ public class AddonRequestedUpdateArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonRequestedUpdateArgs()
+ internal AddonRequestedUpdateArgs()
{
}
@@ -25,12 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs
/// Gets or sets the StringArrayData** for this event.
///
public nint StringArrayData { get; set; }
-
- ///
- internal override void Clear()
- {
- base.Clear();
- this.NumberArrayData = 0;
- this.StringArrayData = 0;
- }
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
index 9b7e86a61..9fd7b6dd0 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
@@ -1,3 +1,5 @@
+using Dalamud.Utility;
+
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@@ -10,8 +12,7 @@ public class AddonSetupArgs : AddonArgs
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonSetupArgs()
+ internal AddonSetupArgs()
{
}
@@ -31,13 +32,7 @@ public class AddonSetupArgs : AddonArgs
///
/// Gets the AtkValues in the form of a span.
///
+ [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")]
+ [Api15ToDo("Remove this")]
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
-
- ///
- internal override void Clear()
- {
- base.Clear();
- this.AtkValueCount = 0;
- this.AtkValues = 0;
- }
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
index bab62fc89..e6147d0eb 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
@@ -1,39 +1,35 @@
+using Dalamud.Utility;
+
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Update events.
///
+[Obsolete("Use AddonGenericArgs instead.")]
+[Api15ToDo("Remove this")]
public class AddonUpdateArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
///
- [Obsolete("Not intended for public construction.", false)]
- public AddonUpdateArgs()
+ internal AddonUpdateArgs()
{
}
///
public override AddonArgsType Type => AddonArgsType.Update;
- ///
- /// Gets the time since the last update.
- ///
- public float TimeDelta
- {
- get => this.TimeDeltaInternal;
- init => this.TimeDeltaInternal = value;
- }
-
///
/// Gets or sets the time since the last update.
///
internal float TimeDeltaInternal { get; set; }
- ///
- internal override void Clear()
+ ///
+ /// Gets the time since the last update.
+ ///
+ private float TimeDelta
{
- base.Clear();
- this.TimeDeltaInternal = 0;
+ get => this.TimeDeltaInternal;
+ init => this.TimeDeltaInternal = value;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
index 9359870a5..2fa3c5b91 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
///
/// AddonLifecycleService memory address resolver.
///
-[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")]
+[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")]
internal class AddonLifecycleAddressResolver : BaseAddressResolver
{
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index d91cd648f..49ffdc7fb 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -26,8 +26,6 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AddonLifecycle lifecycleService;
- // Obsolete warning is only to prevent users from creating their own event objects.
-#pragma warning disable CS0618 // Type or member is obsolete
private readonly AddonSetupArgs addonSetupArg = new();
private readonly AddonFinalizeArgs addonFinalizeArg = new();
private readonly AddonDrawArgs addonDrawArg = new();
@@ -36,7 +34,6 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new();
private readonly AddonReceiveEventArgs addonReceiveEventArg = new();
private readonly AddonGenericArgs addonGenericArg = new();
-#pragma warning restore CS0618 // Type or member is obsolete
private readonly AtkUnitBase* atkUnitBase;
@@ -136,7 +133,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonSetupArg.Clear();
this.addonSetupArg.Addon = addon;
this.addonSetupArg.AtkValueCount = valueCount;
this.addonSetupArg.AtkValues = (nint)values;
@@ -160,7 +156,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonFinalizeArg.Clear();
this.addonFinalizeArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg);
@@ -178,7 +173,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonDrawArg.Clear();
this.addonDrawArg.Addon = addon;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg);
@@ -198,7 +192,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonUpdateArg.Clear();
this.addonUpdateArg.Addon = addon;
this.addonUpdateArg.TimeDeltaInternal = delta;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg);
@@ -221,7 +214,6 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
- this.addonRefreshArg.Clear();
this.addonRefreshArg.Addon = addon;
this.addonRefreshArg.AtkValueCount = valueCount;
this.addonRefreshArg.AtkValues = (nint)values;
@@ -246,7 +238,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonRequestedUpdateArg.Clear();
this.addonRequestedUpdateArg.Addon = addon;
this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData;
this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData;
@@ -270,7 +261,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonReceiveEventArg.Clear();
this.addonReceiveEventArg.Addon = (nint)addon;
this.addonReceiveEventArg.AtkEventType = (byte)eventType;
this.addonReceiveEventArg.EventParam = eventParam;
@@ -300,7 +290,6 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
- this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg);
@@ -324,7 +313,6 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
- this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg);
@@ -346,7 +334,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg);
@@ -366,7 +353,6 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg);
diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
index f5b7011fe..af85f9228 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
@@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
}
///
- [Api13ToDo("Maybe make this config scoped to internal name?")]
+ [Api14ToDo("Maybe make this config scoped to internal name?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
///
diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs
index 0d2057b3b..cc1f48ce7 100644
--- a/Dalamud/Interface/Animation/Easing.cs
+++ b/Dalamud/Interface/Animation/Easing.cs
@@ -48,7 +48,7 @@ public abstract class Easing
/// Gets the current value of the animation, following unclamped logic.
///
[Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)]
- [Api13ToDo("Map this field to ValueClamped, probably.")]
+ [Api14ToDo("Map this field to ValueClamped, probably.")]
public double Value => this.ValueUnclamped;
///
diff --git a/Dalamud/Utility/Api13ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs
similarity index 75%
rename from Dalamud/Utility/Api13ToDoAttribute.cs
rename to Dalamud/Utility/Api14ToDoAttribute.cs
index 576401cda..945b6e4db 100644
--- a/Dalamud/Utility/Api13ToDoAttribute.cs
+++ b/Dalamud/Utility/Api14ToDoAttribute.cs
@@ -4,7 +4,7 @@ namespace Dalamud.Utility;
/// Utility class for marking something to be changed for API 13, for ease of lookup.
///
[AttributeUsage(AttributeTargets.All, Inherited = false)]
-internal sealed class Api13ToDoAttribute : Attribute
+internal sealed class Api14ToDoAttribute : Attribute
{
///
/// Marks that this should be made internal.
@@ -12,11 +12,11 @@ internal sealed class Api13ToDoAttribute : Attribute
public const string MakeInternal = "Make internal.";
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The explanation.
/// The explanation 2.
- public Api13ToDoAttribute(string what, string what2 = "")
+ public Api14ToDoAttribute(string what, string what2 = "")
{
_ = what;
_ = what2;
diff --git a/Dalamud/Utility/Api15ToDoAttribute.cs b/Dalamud/Utility/Api15ToDoAttribute.cs
new file mode 100644
index 000000000..646c260e8
--- /dev/null
+++ b/Dalamud/Utility/Api15ToDoAttribute.cs
@@ -0,0 +1,25 @@
+namespace Dalamud.Utility;
+
+///
+/// Utility class for marking something to be changed for API 13, for ease of lookup.
+/// Intended to represent not the upcoming API, but the one after it for more major changes.
+///
+[AttributeUsage(AttributeTargets.All, Inherited = false)]
+internal sealed class Api15ToDoAttribute : 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 Api15ToDoAttribute(string what, string what2 = "")
+ {
+ _ = what;
+ _ = what2;
+ }
+}
diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs
index 19610ef64..f6abc336c 100644
--- a/Dalamud/Utility/Util.cs
+++ b/Dalamud/Utility/Util.cs
@@ -79,7 +79,7 @@ public static partial class Util
///
/// Gets the Dalamud version.
///
- [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")]
+ [Api14ToDo("Remove. Make both versions here internal. Add an API somewhere.")]
public static string AssemblyVersion { get; } =
Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString();
From 386828005b02d16f24f5710e9aec9453aa3faae5 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 12:37:51 -0800
Subject: [PATCH 071/201] Apply breaking changes
---
.../Lifecycle/AddonArgTypes/AddonArgs.cs | 38 +++---
.../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 21 ---
.../AddonArgTypes/AddonFinalizeArgs.cs | 21 ---
.../AddonArgTypes/AddonGenericArgs.cs | 17 ---
.../AddonArgTypes/AddonReceiveEventArgs.cs | 5 +-
.../AddonArgTypes/AddonUpdateArgs.cs | 35 -----
Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +---
Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 3 -
.../Game/Addon/Lifecycle/AddonLifecycle.cs | 6 +-
.../AddonLifecycleAddressResolver.cs | 24 ----
.../Lifecycle/AddonLifecycleEventListener.cs | 9 +-
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 129 +++++++++---------
.../Windows/Data/Widgets/HookWidget.cs | 36 ++---
.../Internal/Windows/TitleScreenMenuWindow.cs | 4 +-
Directory.Build.props | 2 +-
15 files changed, 114 insertions(+), 261 deletions(-)
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
index 62ca47238..c4a7e8f53 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
@@ -5,19 +5,24 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Base class for AddonLifecycle AddonArgTypes.
///
-public abstract class AddonArgs
+public class AddonArgs
{
///
/// Constant string representing the name of an addon that is invalid.
///
public const string InvalidAddon = "NullAddon";
- private string? addonName;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal AddonArgs()
+ {
+ }
///
/// Gets the name of the addon this args referrers to.
///
- public string AddonName => this.GetAddonName();
+ public string AddonName { get; private set; } = InvalidAddon;
///
/// Gets the pointer to the addons AtkUnitBase.
@@ -25,28 +30,17 @@ public abstract class AddonArgs
public AtkUnitBasePtr Addon
{
get;
- internal set;
+ internal set
+ {
+ field = value;
+
+ if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name))
+ this.AddonName = value.Name;
+ }
}
///
/// Gets the type of these args.
///
- public abstract AddonArgsType Type { get; }
-
- ///
- /// Helper method for ensuring the name of the addon is valid.
- ///
- /// The name of the addon for this object. when invalid.
- private string GetAddonName()
- {
- if (this.Addon.IsNull)
- return InvalidAddon;
-
- var name = this.Addon.Name;
-
- if (string.IsNullOrEmpty(name))
- return InvalidAddon;
-
- return this.addonName ??= name;
- }
+ public virtual AddonArgsType Type => AddonArgsType.Generic;
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
deleted file mode 100644
index a834d2983..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Dalamud.Utility;
-
-namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-
-///
-/// Addon argument data for Draw events.
-///
-[Obsolete("Use AddonGenericArgs instead.")]
-[Api15ToDo("Remove this")]
-public class AddonDrawArgs : AddonArgs
-{
- ///
- /// Initializes a new instance of the class.
- ///
- internal AddonDrawArgs()
- {
- }
-
- ///
- public override AddonArgsType Type => AddonArgsType.Draw;
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
deleted file mode 100644
index 11d15a081..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Dalamud.Utility;
-
-namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-
-///
-/// Addon argument data for ReceiveEvent events.
-///
-[Obsolete("Use AddonGenericArgs instead.")]
-[Api15ToDo("Remove this")]
-public class AddonFinalizeArgs : AddonArgs
-{
- ///
- /// Initializes a new instance of the class.
- ///
- internal AddonFinalizeArgs()
- {
- }
-
- ///
- public override AddonArgsType Type => AddonArgsType.Finalize;
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
deleted file mode 100644
index a20e9d23b..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-
-///
-/// Addon argument data for Draw events.
-///
-public class AddonGenericArgs : AddonArgs
-{
- ///
- /// Initializes a new instance of the class.
- ///
- internal AddonGenericArgs()
- {
- }
-
- ///
- public override AddonArgsType Type => AddonArgsType.Generic;
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
index bb8168075..785cd199f 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
@@ -1,5 +1,3 @@
-using Dalamud.Utility;
-
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
@@ -35,6 +33,5 @@ public class AddonReceiveEventArgs : AddonArgs
///
/// Gets or sets the pointer to an AtkEventData for this event message.
///
- [Api14ToDo("Rename to AtkEventData")]
- public nint Data { get; set; }
+ public nint AtkEventData { get; set; }
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
deleted file mode 100644
index e6147d0eb..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Dalamud.Utility;
-
-namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-
-///
-/// Addon argument data for Update events.
-///
-[Obsolete("Use AddonGenericArgs instead.")]
-[Api15ToDo("Remove this")]
-public class AddonUpdateArgs : AddonArgs
-{
- ///
- /// Initializes a new instance of the class.
- ///
- internal AddonUpdateArgs()
- {
- }
-
- ///
- public override AddonArgsType Type => AddonArgsType.Update;
-
- ///
- /// Gets or sets the time since the last update.
- ///
- internal float TimeDeltaInternal { get; set; }
-
- ///
- /// Gets the time since the last update.
- ///
- private float TimeDelta
- {
- get => this.TimeDeltaInternal;
- init => this.TimeDeltaInternal = value;
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
index de32bd254..9d7815cef 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
@@ -5,26 +5,16 @@
///
public enum AddonArgsType
{
+ ///
+ /// Generic arg type that contains no meaningful data.
+ ///
+ Generic,
+
///
/// Contains argument data for Setup.
///
Setup,
- ///
- /// Contains argument data for Update.
- ///
- Update,
-
- ///
- /// Contains argument data for Draw.
- ///
- Draw,
-
- ///
- /// Contains argument data for Finalize.
- ///
- Finalize,
-
///
/// Contains argument data for RequestedUpdate.
///
@@ -39,9 +29,4 @@ public enum AddonArgsType
/// Contains argument data for ReceiveEvent.
///
ReceiveEvent,
-
- ///
- /// Generic arg type that contains no meaningful data.
- ///
- Generic,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
index 7738d6c6a..5ec57b5e3 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
@@ -29,7 +29,6 @@ public enum AddonEvent
/// An event that is fired before an addon begins its update cycle via . This event
/// is fired every frame that an addon is loaded, regardless of visibility.
///
- ///
PreUpdate,
///
@@ -42,7 +41,6 @@ public enum AddonEvent
/// An event that is fired before an addon begins drawing to screen via . Unlike
/// , this event is only fired if an addon is visible or otherwise drawing to screen.
///
- ///
PreDraw,
///
@@ -62,7 +60,6 @@ public enum AddonEvent
///
/// As this is part of the destruction process for an addon, this event does not have an associated Post event.
///
- ///
PreFinalize,
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index 5d121bea4..ddcebe718 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -59,13 +59,15 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{
if (!this.EventListeners.ContainsKey(listener.EventType))
{
- this.EventListeners.TryAdd(listener.EventType, []);
+ if (!this.EventListeners.TryAdd(listener.EventType, []))
+ return;
}
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
{
- this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []);
+ if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
+ return;
}
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
deleted file mode 100644
index 2fa3c5b91..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Dalamud.Utility;
-
-namespace Dalamud.Game.Addon.Lifecycle;
-
-///
-/// AddonLifecycleService memory address resolver.
-///
-[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")]
-internal class AddonLifecycleAddressResolver : BaseAddressResolver
-{
- ///
- /// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
- ///
- public nint AddonFinalize { get; private set; }
-
- ///
- /// Scan for and setup any configured address pointers.
- ///
- /// The signature scanner to facilitate setup.
- protected override void Setup64Bit(ISigScanner sig)
- {
- this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs
index 9d411cdbc..fc82e0582 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs
@@ -25,17 +25,12 @@ internal class AddonLifecycleEventListener
/// string.Empty if it wants to be called for any addon.
///
public string AddonName { get; init; }
-
- ///
- /// Gets or sets a value indicating whether this event has been unregistered.
- ///
- public bool Removed { get; set; }
-
+
///
/// Gets the event type this listener is looking for.
///
public AddonEvent EventType { get; init; }
-
+
///
/// Gets the delegate this listener invokes.
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index 49ffdc7fb..1ce145946 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -26,14 +26,18 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AddonLifecycle lifecycleService;
- private readonly AddonSetupArgs addonSetupArg = new();
- private readonly AddonFinalizeArgs addonFinalizeArg = new();
- private readonly AddonDrawArgs addonDrawArg = new();
- private readonly AddonUpdateArgs addonUpdateArg = new();
- private readonly AddonRefreshArgs addonRefreshArg = new();
- private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new();
- private readonly AddonReceiveEventArgs addonReceiveEventArg = new();
- private readonly AddonGenericArgs addonGenericArg = new();
+ // Each addon gets its own set of args that are used to mutate the original call when used in pre-calls
+ private readonly AddonSetupArgs setupArgs = new();
+ private readonly AddonArgs finalizeArgs = new();
+ private readonly AddonArgs drawArgs = new();
+ private readonly AddonArgs updateArgs = new();
+ private readonly AddonRefreshArgs refreshArgs = new();
+ private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new();
+ private readonly AddonReceiveEventArgs receiveEventArgs = new();
+ private readonly AddonArgs openArgs = new();
+ private readonly AddonArgs closeArgs = new();
+ private readonly AddonArgs showArgs = new();
+ private readonly AddonArgs hideArgs = new();
private readonly AtkUnitBase* atkUnitBase;
@@ -133,12 +137,13 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonSetupArg.Addon = addon;
- this.addonSetupArg.AtkValueCount = valueCount;
- this.addonSetupArg.AtkValues = (nint)values;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg);
- valueCount = this.addonSetupArg.AtkValueCount;
- values = (AtkValue*)this.addonSetupArg.AtkValues;
+ this.setupArgs.Addon = addon;
+ this.setupArgs.AtkValueCount = valueCount;
+ this.setupArgs.AtkValues = (nint)values;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs);
+
+ valueCount = this.setupArgs.AtkValueCount;
+ values = (AtkValue*)this.setupArgs.AtkValues;
try
{
@@ -149,15 +154,15 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs);
}
private void OnAddonFinalize(AtkUnitBase* thisPtr)
{
this.LogEvent(EnableLogging);
- this.addonFinalizeArg.Addon = thisPtr;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg);
+ this.finalizeArgs.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs);
try
{
@@ -173,8 +178,8 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonDrawArg.Addon = addon;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg);
+ this.drawArgs.Addon = addon;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs);
try
{
@@ -185,16 +190,15 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
this.LogEvent(EnableLogging);
- this.addonUpdateArg.Addon = addon;
- this.addonUpdateArg.TimeDeltaInternal = delta;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg);
+ this.updateArgs.Addon = addon;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs);
try
{
@@ -205,7 +209,7 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs);
}
private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
@@ -214,12 +218,13 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
- this.addonRefreshArg.Addon = addon;
- this.addonRefreshArg.AtkValueCount = valueCount;
- this.addonRefreshArg.AtkValues = (nint)values;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg);
- valueCount = this.addonRefreshArg.AtkValueCount;
- values = (AtkValue*)this.addonRefreshArg.AtkValues;
+ this.refreshArgs.Addon = addon;
+ this.refreshArgs.AtkValueCount = valueCount;
+ this.refreshArgs.AtkValues = (nint)values;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs);
+
+ valueCount = this.refreshArgs.AtkValueCount;
+ values = (AtkValue*)this.refreshArgs.AtkValues;
try
{
@@ -230,7 +235,7 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs);
return result;
}
@@ -238,12 +243,13 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonRequestedUpdateArg.Addon = addon;
- this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData;
- this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg);
- numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData;
- stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData;
+ this.requestedUpdateArgs.Addon = addon;
+ this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
+ this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs);
+
+ numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData;
+ stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData;
try
{
@@ -254,23 +260,24 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs);
}
private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
this.LogEvent(EnableLogging);
- this.addonReceiveEventArg.Addon = (nint)addon;
- this.addonReceiveEventArg.AtkEventType = (byte)eventType;
- this.addonReceiveEventArg.EventParam = eventParam;
- this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent;
- this.addonReceiveEventArg.Data = (nint)atkEventData;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg);
- eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType;
- eventParam = this.addonReceiveEventArg.EventParam;
- atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent;
- atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data;
+ this.receiveEventArgs.Addon = (nint)addon;
+ this.receiveEventArgs.AtkEventType = (byte)eventType;
+ this.receiveEventArgs.EventParam = eventParam;
+ this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent;
+ this.receiveEventArgs.AtkEventData = (nint)atkEventData;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs);
+
+ eventType = (AtkEventType)this.receiveEventArgs.AtkEventType;
+ eventParam = this.receiveEventArgs.EventParam;
+ atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent;
+ atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData;
try
{
@@ -281,7 +288,7 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs);
}
private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
@@ -290,8 +297,8 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
- this.addonGenericArg.Addon = thisPtr;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg);
+ this.openArgs.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs);
try
{
@@ -302,7 +309,7 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs);
return result;
}
@@ -313,8 +320,8 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
- this.addonGenericArg.Addon = thisPtr;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg);
+ this.closeArgs.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs);
try
{
@@ -325,7 +332,7 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs);
return result;
}
@@ -334,8 +341,8 @@ internal unsafe class AddonVirtualTable : IDisposable
{
this.LogEvent(EnableLogging);
- this.addonGenericArg.Addon = thisPtr;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg);
+ this.showArgs.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs);
try
{
@@ -346,15 +353,15 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs);
}
private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
{
this.LogEvent(EnableLogging);
- this.addonGenericArg.Addon = thisPtr;
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg);
+ this.hideArgs.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs);
try
{
@@ -365,7 +372,7 @@ internal unsafe class AddonVirtualTable : IDisposable
Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method.");
}
- this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg);
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs);
}
[Conditional("DEBUG")]
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs
index 3ad8f86c2..c5ae1d8f0 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs
@@ -5,9 +5,8 @@ using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Game;
-using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.ClientState;
using Dalamud.Hooking;
-using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
@@ -17,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
///
/// Widget for displaying hook information.
///
-internal unsafe class HookWidget : IDataWindowWidget
+internal class HookWidget : IDataWindowWidget
{
private readonly List hookStressTestList = [];
@@ -32,9 +31,9 @@ internal unsafe class HookWidget : IDataWindowWidget
private bool hookStressTestRunning = false;
private MessageBoxWDelegate? messageBoxWOriginal;
- private AddonFinalizeDelegate? addonFinalizeOriginal;
+ private HandleZoneInitPacketDelegate? zoneInitOriginal;
- private AddonLifecycleAddressResolver? address;
+ private ClientStateAddressResolver? address;
private delegate int MessageBoxWDelegate(
IntPtr hWnd,
@@ -42,12 +41,12 @@ internal unsafe class HookWidget : IDataWindowWidget
[MarshalAs(UnmanagedType.LPWStr)] string caption,
MESSAGEBOX_STYLE type);
- private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
+ private delegate void HandleZoneInitPacketDelegate(nint a1, uint localPlayerEntityId, nint packet, byte type);
private enum StressTestHookTarget
{
MessageBoxW,
- AddonFinalize,
+ ZoneInit,
Random,
}
@@ -65,7 +64,7 @@ internal unsafe class HookWidget : IDataWindowWidget
{
this.Ready = true;
- this.address = new AddonLifecycleAddressResolver();
+ this.address = new ClientStateAddressResolver();
this.address.Setup(Service.Get());
}
@@ -179,7 +178,7 @@ internal unsafe class HookWidget : IDataWindowWidget
return target switch
{
StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)",
- StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)",
+ StressTestHookTarget.ZoneInit => "ZoneInit (Hook)",
_ => target.ToString(),
};
}
@@ -198,15 +197,10 @@ internal unsafe class HookWidget : IDataWindowWidget
return result;
}
- private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
+ private void OnZoneInit(IntPtr a1, uint localPlayerEntityId, IntPtr packet, byte type)
{
- Log.Information("OnAddonFinalize");
- this.addonFinalizeOriginal!(unitManager, atkUnitBase);
- }
-
- private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta)
- {
- Log.Information("OnAddonUpdate");
+ Log.Information("OnZoneInit");
+ this.zoneInitOriginal!.Invoke(a1, localPlayerEntityId, packet, type);
}
private IDalamudHook HookMessageBoxW()
@@ -222,11 +216,11 @@ internal unsafe class HookWidget : IDataWindowWidget
return hook;
}
- private IDalamudHook HookAddonFinalize()
+ private IDalamudHook HookZoneInit()
{
- var hook = Hook.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize);
+ var hook = Hook.FromAddress(this.address!.HandleZoneInitPacket, this.OnZoneInit);
- this.addonFinalizeOriginal = hook.Original;
+ this.zoneInitOriginal = hook.Original;
hook.Enable();
return hook;
}
@@ -241,7 +235,7 @@ internal unsafe class HookWidget : IDataWindowWidget
return target switch
{
StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(),
- StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(),
+ StressTestHookTarget.ZoneInit => this.HookZoneInit(),
_ => throw new ArgumentOutOfRangeException(nameof(target), target, null),
};
}
diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
index e3eb22a04..62f83f82f 100644
--- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
@@ -471,9 +471,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable
private unsafe void OnVersionStringDraw(AddonEvent ev, AddonArgs args)
{
- if (args is not AddonDrawArgs drawArgs) return;
+ if (ev is not (AddonEvent.PostDraw or AddonEvent.PreDraw)) return;
- var addon = drawArgs.Addon.Struct;
+ var addon = args.Addon.Struct;
var textNode = addon->GetTextNodeById(3);
// look and feel init. should be harmless to set.
diff --git a/Directory.Build.props b/Directory.Build.props
index eabb727e8..3897256bf 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -5,7 +5,7 @@
net10.0-windows
x64
x64
- 13.0
+ 14.0
From d47a41b2953407335fcb136f4843ea8e096306fc Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 12:48:49 -0800
Subject: [PATCH 072/201] Fix NET14 Spans defaulting to ReadOnlySpan
---
imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs | 6 +++---
imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs | 6 +++---
imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs | 6 +++---
imgui/Dalamud.Bindings.ImGui/ImU8String.cs | 4 ++--
4 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs
index 665fa434f..3cf20bb30 100644
--- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs
+++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs
@@ -238,7 +238,7 @@ public static unsafe partial class ImGui
ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar(
label,
ImGuiDataType.Float,
- MemoryMarshal.Cast(new(ref v)),
+ MemoryMarshal.Cast(new Span(ref v)),
vSpeed,
vMin,
vMax,
@@ -251,7 +251,7 @@ public static unsafe partial class ImGui
ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar(
label,
ImGuiDataType.Float,
- MemoryMarshal.Cast(new(ref v)),
+ MemoryMarshal.Cast(new Span(ref v)),
vSpeed,
vMin,
vMax,
@@ -264,7 +264,7 @@ public static unsafe partial class ImGui
ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar(
label,
ImGuiDataType.Float,
- MemoryMarshal.Cast(new(ref v)),
+ MemoryMarshal.Cast(new Span(ref v)),
vSpeed,
vMin,
vMax,
diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs
index fb86096ff..5881ac462 100644
--- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs
+++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs
@@ -205,7 +205,7 @@ public static unsafe partial class ImGui
InputScalar(
label,
ImGuiDataType.Float,
- MemoryMarshal.Cast(new(ref data)),
+ MemoryMarshal.Cast(new Span(ref data)),
step,
stepFast,
format.MoveOrDefault("%.3f"u8),
@@ -219,7 +219,7 @@ public static unsafe partial class ImGui
InputScalar(
label,
ImGuiDataType.Float,
- MemoryMarshal.Cast(new(ref data)),
+ MemoryMarshal.Cast(new Span(ref data)),
step,
stepFast,
format.MoveOrDefault("%.3f"u8),
@@ -233,7 +233,7 @@ public static unsafe partial class ImGui
InputScalar(
label,
ImGuiDataType.Float,
- MemoryMarshal.Cast(new(ref data)),
+ MemoryMarshal.Cast(new Span(ref data)),
step,
stepFast,
format.MoveOrDefault("%.3f"u8),
diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs
index 20ee78ab6..b0c4b7c79 100644
--- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs
+++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs
@@ -210,7 +210,7 @@ public static unsafe partial class ImGui
ImU8String format = default, ImGuiSliderFlags flags = ImGuiSliderFlags.None) => SliderScalar(
label,
ImGuiDataType.Float,
- MemoryMarshal.Cast(new(ref v)),
+ MemoryMarshal.Cast(new Span(ref v)),
vMin,
vMax,
format.MoveOrDefault("%.3f"u8),
@@ -222,7 +222,7 @@ public static unsafe partial class ImGui
SliderScalar(
label,
ImGuiDataType.Float,
- MemoryMarshal.Cast(new(ref v)),
+ MemoryMarshal.Cast(new Span(ref v)),
vMin,
vMax,
format.MoveOrDefault("%.3f"u8),
@@ -236,7 +236,7 @@ public static unsafe partial class ImGui
SliderScalar(
label,
ImGuiDataType.Float,
- MemoryMarshal.Cast(new(ref v)),
+ MemoryMarshal.Cast(new Span(ref v)),
vMin,
vMax,
format.MoveOrDefault("%.3f"u8),
diff --git a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs
index a62152c39..f2b635764 100644
--- a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs
+++ b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs
@@ -156,7 +156,7 @@ public ref struct ImU8String
return this.rentedBuffer is { } buf
? buf.AsSpan()
- : MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer)));
+ : MemoryMarshal.Cast(new Span(ref Unsafe.AsRef(ref this.fixedBuffer)));
}
}
@@ -165,7 +165,7 @@ public ref struct ImU8String
private ref byte FixedBufferByteRef => ref this.FixedBufferSpan[0];
private Span FixedBufferSpan =>
- MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer)));
+ MemoryMarshal.Cast(new Span(ref Unsafe.AsRef(ref this.fixedBuffer)));
public static implicit operator ImU8String(ReadOnlySpan text) => new(text);
public static implicit operator ImU8String(ReadOnlyMemory text) => new(text);
From 8e8d0246bc40d4b7d172c48ad2bc076882811102 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 14:00:39 -0800
Subject: [PATCH 073/201] Restore original hookwidget logic
---
.../Windows/Data/Widgets/HookWidget.cs | 39 +++++++++++--------
1 file changed, 22 insertions(+), 17 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs
index c5ae1d8f0..f3e25caf8 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs
@@ -5,8 +5,8 @@ using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Game;
-using Dalamud.Game.ClientState;
using Dalamud.Hooking;
+using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
@@ -16,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
///
/// Widget for displaying hook information.
///
-internal class HookWidget : IDataWindowWidget
+internal unsafe class HookWidget : IDataWindowWidget
{
private readonly List hookStressTestList = [];
@@ -31,9 +31,9 @@ internal class HookWidget : IDataWindowWidget
private bool hookStressTestRunning = false;
private MessageBoxWDelegate? messageBoxWOriginal;
- private HandleZoneInitPacketDelegate? zoneInitOriginal;
+ private AddonFinalizeDelegate? addonFinalizeOriginal;
- private ClientStateAddressResolver? address;
+ private nint address;
private delegate int MessageBoxWDelegate(
IntPtr hWnd,
@@ -41,12 +41,12 @@ internal class HookWidget : IDataWindowWidget
[MarshalAs(UnmanagedType.LPWStr)] string caption,
MESSAGEBOX_STYLE type);
- private delegate void HandleZoneInitPacketDelegate(nint a1, uint localPlayerEntityId, nint packet, byte type);
+ private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
private enum StressTestHookTarget
{
MessageBoxW,
- ZoneInit,
+ AddonFinalize,
Random,
}
@@ -54,7 +54,7 @@ internal class HookWidget : IDataWindowWidget
public string DisplayName { get; init; } = "Hook";
///
- public string[]? CommandShortcuts { get; init; } = { "hook" };
+ public string[]? CommandShortcuts { get; init; } = ["hook"];
///
public bool Ready { get; set; }
@@ -64,8 +64,8 @@ internal class HookWidget : IDataWindowWidget
{
this.Ready = true;
- this.address = new ClientStateAddressResolver();
- this.address.Setup(Service.Get());
+ var sigScanner = Service.Get();
+ this.address = sigScanner.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
}
///
@@ -178,7 +178,7 @@ internal class HookWidget : IDataWindowWidget
return target switch
{
StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)",
- StressTestHookTarget.ZoneInit => "ZoneInit (Hook)",
+ StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)",
_ => target.ToString(),
};
}
@@ -197,10 +197,15 @@ internal class HookWidget : IDataWindowWidget
return result;
}
- private void OnZoneInit(IntPtr a1, uint localPlayerEntityId, IntPtr packet, byte type)
+ private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
{
- Log.Information("OnZoneInit");
- this.zoneInitOriginal!.Invoke(a1, localPlayerEntityId, packet, type);
+ Log.Information("OnAddonFinalize");
+ this.addonFinalizeOriginal!(unitManager, atkUnitBase);
+ }
+
+ private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta)
+ {
+ Log.Information("OnAddonUpdate");
}
private IDalamudHook HookMessageBoxW()
@@ -216,11 +221,11 @@ internal class HookWidget : IDataWindowWidget
return hook;
}
- private IDalamudHook HookZoneInit()
+ private IDalamudHook HookAddonFinalize()
{
- var hook = Hook.FromAddress(this.address!.HandleZoneInitPacket, this.OnZoneInit);
+ var hook = Hook.FromAddress(this.address, this.OnAddonFinalize);
- this.zoneInitOriginal = hook.Original;
+ this.addonFinalizeOriginal = hook.Original;
hook.Enable();
return hook;
}
@@ -235,7 +240,7 @@ internal class HookWidget : IDataWindowWidget
return target switch
{
StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(),
- StressTestHookTarget.ZoneInit => this.HookZoneInit(),
+ StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(),
_ => throw new ArgumentOutOfRangeException(nameof(target), target, null),
};
}
From b81cb9c74c7e8fc5423f1568904d807daeca4115 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 14:07:44 -0800
Subject: [PATCH 074/201] Remove generic args class
---
.../Lifecycle/AddonArgTypes/AddonGenericArgs.cs | 17 -----------------
1 file changed, 17 deletions(-)
delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
deleted file mode 100644
index a20e9d23b..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-
-///
-/// Addon argument data for Draw events.
-///
-public class AddonGenericArgs : AddonArgs
-{
- ///
- /// Initializes a new instance of the class.
- ///
- internal AddonGenericArgs()
- {
- }
-
- ///
- public override AddonArgsType Type => AddonArgsType.Generic;
-}
From 2e246967317427d2d5749d69b24a3e8ebc77d328 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 14:47:24 -0800
Subject: [PATCH 075/201] Set flags, and unlock size
---
Dalamud/Interface/Windowing/Window.cs | 26 ++++++++++----------------
1 file changed, 10 insertions(+), 16 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index 5169b9746..700481ce5 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -458,26 +458,20 @@ public abstract class Window
if (this.internalIsPinned || this.internalIsClickthrough)
{
- if (!this.hasError)
- {
- flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
- }
- else
- {
- flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize);
- }
+ flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
}
if (this.internalIsClickthrough)
{
- if (!this.hasError)
- {
- flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
- }
- else
- {
- flags &= ~(ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs);
- }
+ flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
+ }
+
+ // If we have an error, reset all flags to default, and unlock window size.
+ if (this.hasError)
+ {
+ flags = ImGuiWindowFlags.None;
+ ImGui.SetNextWindowCollapsed(false, ImGuiCond.Once);
+ ImGui.SetNextWindowSizeConstraints(Vector2.Zero, Vector2.PositiveInfinity);
}
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
From 78781c8988bbd67be3ce6514fd2de1282c052163 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 21:43:26 -0800
Subject: [PATCH 076/201] Add Move, MouseOver, MouseOut, Focus
---
.../Lifecycle/AddonArgTypes/AddonCloseArgs.cs | 22 ++++
.../Lifecycle/AddonArgTypes/AddonHideArgs.cs | 32 +++++
.../Lifecycle/AddonArgTypes/AddonShowArgs.cs | 27 ++++
Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 15 +++
Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 60 ++++++++-
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 118 +++++++++++++++++-
6 files changed, 264 insertions(+), 10 deletions(-)
create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs
create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs
create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs
new file mode 100644
index 000000000..db3e442f8
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs
@@ -0,0 +1,22 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Close events.
+///
+public class AddonCloseArgs : AddonArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal AddonCloseArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Close;
+
+ ///
+ /// Gets or sets a value indicating whether the window should fire the callback method on close.
+ ///
+ public bool FireCallback { get; set; }
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs
new file mode 100644
index 000000000..3e3521bd0
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs
@@ -0,0 +1,32 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Hide events.
+///
+public class AddonHideArgs : AddonArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal AddonHideArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Hide;
+
+ ///
+ /// Gets or sets a value indicating whether to call the hide callback handler when this hides.
+ ///
+ public bool CallHideCallback { get; set; }
+
+ ///
+ /// Gets or sets the flags that the window will set when it Shows/Hides.
+ ///
+ public uint SetShowHideFlags { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether something for this event message.
+ ///
+ internal bool UnknownBool { get; set; }
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs
new file mode 100644
index 000000000..3153d1208
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs
@@ -0,0 +1,27 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Show events.
+///
+public class AddonShowArgs : AddonArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal AddonShowArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Show;
+
+ ///
+ /// Gets or sets a value indicating whether the window should play open sound effects.
+ ///
+ public bool SilenceOpenSoundEffect { get; set; }
+
+ ///
+ /// Gets or sets the flags that the window will unset when it Shows/Hides.
+ ///
+ public uint UnsetShowHideFlags { get; set; }
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
index 9d7815cef..46ee479ac 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
@@ -29,4 +29,19 @@ public enum AddonArgsType
/// Contains argument data for ReceiveEvent.
///
ReceiveEvent,
+
+ ///
+ /// Contains argument data for Show.
+ ///
+ Show,
+
+ ///
+ /// Contains argument data for Hide.
+ ///
+ Hide,
+
+ ///
+ /// Contains argument data for Close.
+ ///
+ Close,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
index 5ec57b5e3..3b9c6e867 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
@@ -127,32 +127,80 @@ public enum AddonEvent
PostOpen,
///
- /// An even that is fired before an addon processes its close method.
+ /// An even that is fired before an addon processes its Close method.
///
PreClose,
///
- /// An event that is fired after an addon has processed its close method.
+ /// An event that is fired after an addon has processed its Close method.
///
PostClose,
///
- /// An event that is fired before an addon processes its show method.
+ /// An event that is fired before an addon processes its Show method.
///
PreShow,
///
- /// An event that is fired after an addon has processed its show method.
+ /// An event that is fired after an addon has processed its Show method.
///
PostShow,
///
- /// An event that is fired before an addon processes its hide method.
+ /// An event that is fired before an addon processes its Hide method.
///
PreHide,
///
- /// An event that is fired after an addon has processed its hide method.
+ /// An event that is fired after an addon has processed its Hide method.
///
PostHide,
+
+ ///
+ /// An event that is fired before an addon processes its OnMove method.
+ /// OnMove is triggered only when a move is completed.
+ ///
+ PreMove,
+
+ ///
+ /// An event that is fired after an addon has processed its OnMove method.
+ /// OnMove is triggered only when a move is completed.
+ ///
+ PostMove,
+
+ ///
+ /// An event that is fired before an addon processes its MouseOver method.
+ ///
+ PreMouseOver,
+
+ ///
+ /// An event that is fired after an addon has processed its MouseOver method.
+ ///
+ PostMouseOver,
+
+ ///
+ /// An event that is fired before an addon processes its MouseOut method.
+ ///
+ PreMouseOut,
+
+ ///
+ /// An event that is fired after an addon has processed its MouseOut method.
+ ///
+ PostMouseOut,
+
+ ///
+ /// An event that is fired before an addon processes its Focus method.
+ ///
+ ///
+ /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
+ ///
+ PreFocus,
+
+ ///
+ /// An event that is fired after an addon has processed its Focus method.
+ ///
+ ///
+ /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
+ ///
+ PostFocus,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index 1ce145946..b92466b5a 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -20,7 +20,7 @@ internal unsafe class AddonVirtualTable : IDisposable
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 200;
- private const bool EnableLogging = false;
+ private const bool EnableLogging = true;
private static readonly ModuleLog Log = new("LifecycleVT");
@@ -35,9 +35,13 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new();
private readonly AddonReceiveEventArgs receiveEventArgs = new();
private readonly AddonArgs openArgs = new();
- private readonly AddonArgs closeArgs = new();
- private readonly AddonArgs showArgs = new();
- private readonly AddonArgs hideArgs = new();
+ private readonly AddonCloseArgs closeArgs = new();
+ private readonly AddonShowArgs showArgs = new();
+ private readonly AddonHideArgs hideArgs = new();
+ private readonly AddonArgs onMoveArgs = new();
+ private readonly AddonArgs onMouseOverArgs = new();
+ private readonly AddonArgs onMouseOutArgs = new();
+ private readonly AddonArgs focusArgs = new();
private readonly AtkUnitBase* atkUnitBase;
@@ -58,6 +62,10 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AtkUnitBase.Delegates.Close closeFunction;
private readonly AtkUnitBase.Delegates.Show showFunction;
private readonly AtkUnitBase.Delegates.Hide hideFunction;
+ private readonly AtkUnitBase.Delegates.OnMove onMoveFunction;
+ private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction;
+ private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction;
+ private readonly AtkUnitBase.Delegates.Focus focusFunction;
///
/// Initializes a new instance of the class.
@@ -94,6 +102,10 @@ internal unsafe class AddonVirtualTable : IDisposable
this.closeFunction = this.OnAddonClose;
this.showFunction = this.OnAddonShow;
this.hideFunction = this.OnAddonHide;
+ this.onMoveFunction = this.OnMove;
+ this.onMouseOverFunction = this.OnMouseOver;
+ this.onMouseOutFunction = this.OnMouseOut;
+ this.focusFunction = this.OnFocus;
// Overwrite specific virtual table entries
this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
@@ -108,6 +120,10 @@ internal unsafe class AddonVirtualTable : IDisposable
this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
+ this.modifiedVirtualTable->OnMove = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction);
+ this.modifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
+ this.modifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
+ this.modifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
}
///
@@ -200,6 +216,9 @@ internal unsafe class AddonVirtualTable : IDisposable
this.updateArgs.Addon = addon;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs);
+ // Note: Do not pass or allow manipulation of delta.
+ // It's realistically not something that should be needed.
+
try
{
this.originalVirtualTable->Update(addon, delta);
@@ -321,8 +340,11 @@ internal unsafe class AddonVirtualTable : IDisposable
var result = false;
this.closeArgs.Addon = thisPtr;
+ this.closeArgs.FireCallback = fireCallback;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs);
+ fireCallback = this.closeArgs.FireCallback;
+
try
{
result = this.originalVirtualTable->Close(thisPtr, fireCallback);
@@ -342,8 +364,13 @@ internal unsafe class AddonVirtualTable : IDisposable
this.LogEvent(EnableLogging);
this.showArgs.Addon = thisPtr;
+ this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect;
+ this.showArgs.UnsetShowHideFlags = unsetShowHideFlags;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs);
+ silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect;
+ unsetShowHideFlags = this.showArgs.UnsetShowHideFlags;
+
try
{
this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
@@ -361,8 +388,15 @@ internal unsafe class AddonVirtualTable : IDisposable
this.LogEvent(EnableLogging);
this.hideArgs.Addon = thisPtr;
+ this.hideArgs.UnknownBool = unkBool;
+ this.hideArgs.CallHideCallback = callHideCallback;
+ this.hideArgs.SetShowHideFlags = setShowHideFlags;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs);
+ unkBool = this.hideArgs.UnknownBool;
+ callHideCallback = this.hideArgs.CallHideCallback;
+ setShowHideFlags = this.hideArgs.SetShowHideFlags;
+
try
{
this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
@@ -375,6 +409,82 @@ internal unsafe class AddonVirtualTable : IDisposable
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs);
}
+ private void OnMove(AtkUnitBase* thisPtr)
+ {
+ this.LogEvent(EnableLogging);
+
+ this.onMoveArgs.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs);
+
+ try
+ {
+ this.originalVirtualTable->OnMove(thisPtr);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original OnMove. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs);
+ }
+
+ private void OnMouseOver(AtkUnitBase* thisPtr)
+ {
+ this.LogEvent(EnableLogging);
+
+ this.onMouseOverArgs.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs);
+
+ try
+ {
+ this.originalVirtualTable->OnMouseOver(thisPtr);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original OnMouseOver. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs);
+ }
+
+ private void OnMouseOut(AtkUnitBase* thisPtr)
+ {
+ this.LogEvent(EnableLogging);
+
+ this.onMouseOutArgs.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs);
+
+ try
+ {
+ this.originalVirtualTable->OnMouseOut(thisPtr);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original OnMouseOut. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs);
+ }
+
+ private void OnFocus(AtkUnitBase* thisPtr)
+ {
+ this.LogEvent(EnableLogging);
+
+ this.focusArgs.Addon = thisPtr;
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs);
+
+ try
+ {
+ this.originalVirtualTable->Focus(thisPtr);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original OnFocus. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs);
+ }
+
[Conditional("DEBUG")]
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
{
From c923884626fa52470700a65d5f0c8236c7905238 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 22:15:32 -0800
Subject: [PATCH 077/201] Disable Logging
---
Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index b92466b5a..6a27cc8f9 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -20,7 +20,7 @@ internal unsafe class AddonVirtualTable : IDisposable
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 200;
- private const bool EnableLogging = true;
+ private const bool EnableLogging = false;
private static readonly ModuleLog Log = new("LifecycleVT");
From 85a7c60daedf7bf85dc0b4c914f1ff3fd9689c1f Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 22:20:02 -0800
Subject: [PATCH 078/201] Fix name inconsistency
---
.../Game/Addon/Lifecycle/AddonVirtualTable.cs | 24 +++++++++----------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
index 6a27cc8f9..8fbf77534 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -102,10 +102,10 @@ internal unsafe class AddonVirtualTable : IDisposable
this.closeFunction = this.OnAddonClose;
this.showFunction = this.OnAddonShow;
this.hideFunction = this.OnAddonHide;
- this.onMoveFunction = this.OnMove;
- this.onMouseOverFunction = this.OnMouseOver;
- this.onMouseOutFunction = this.OnMouseOut;
- this.focusFunction = this.OnFocus;
+ this.onMoveFunction = this.OnAddonMove;
+ this.onMouseOverFunction = this.OnAddonMouseOver;
+ this.onMouseOutFunction = this.OnAddonMouseOut;
+ this.focusFunction = this.OnAddonFocus;
// Overwrite specific virtual table entries
this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
@@ -409,7 +409,7 @@ internal unsafe class AddonVirtualTable : IDisposable
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs);
}
- private void OnMove(AtkUnitBase* thisPtr)
+ private void OnAddonMove(AtkUnitBase* thisPtr)
{
this.LogEvent(EnableLogging);
@@ -422,13 +422,13 @@ internal unsafe class AddonVirtualTable : IDisposable
}
catch (Exception e)
{
- Log.Error(e, "Caught exception when calling original OnMove. This may be a bug in the game or another plugin hooking this method.");
+ Log.Error(e, "Caught exception when calling original OnAddonMove. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs);
}
- private void OnMouseOver(AtkUnitBase* thisPtr)
+ private void OnAddonMouseOver(AtkUnitBase* thisPtr)
{
this.LogEvent(EnableLogging);
@@ -441,13 +441,13 @@ internal unsafe class AddonVirtualTable : IDisposable
}
catch (Exception e)
{
- Log.Error(e, "Caught exception when calling original OnMouseOver. This may be a bug in the game or another plugin hooking this method.");
+ Log.Error(e, "Caught exception when calling original OnAddonMouseOver. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs);
}
- private void OnMouseOut(AtkUnitBase* thisPtr)
+ private void OnAddonMouseOut(AtkUnitBase* thisPtr)
{
this.LogEvent(EnableLogging);
@@ -460,13 +460,13 @@ internal unsafe class AddonVirtualTable : IDisposable
}
catch (Exception e)
{
- Log.Error(e, "Caught exception when calling original OnMouseOut. This may be a bug in the game or another plugin hooking this method.");
+ Log.Error(e, "Caught exception when calling original OnAddonMouseOut. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs);
}
- private void OnFocus(AtkUnitBase* thisPtr)
+ private void OnAddonFocus(AtkUnitBase* thisPtr)
{
this.LogEvent(EnableLogging);
@@ -479,7 +479,7 @@ internal unsafe class AddonVirtualTable : IDisposable
}
catch (Exception e)
{
- Log.Error(e, "Caught exception when calling original OnFocus. This may be a bug in the game or another plugin hooking this method.");
+ Log.Error(e, "Caught exception when calling original OnAddonFocus. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs);
From fb229a0a128dd36b3e531be7bd00d260649ce379 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Mon, 1 Dec 2025 12:07:34 +0100
Subject: [PATCH 079/201] Fix PlayerState.Level being synced
---
Dalamud/Game/Player/PlayerState.cs | 2 +-
Dalamud/Plugin/Services/IPlayerState.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Game/Player/PlayerState.cs b/Dalamud/Game/Player/PlayerState.cs
index 917c946db..bd19b5bfb 100644
--- a/Dalamud/Game/Player/PlayerState.cs
+++ b/Dalamud/Game/Player/PlayerState.cs
@@ -77,7 +77,7 @@ internal unsafe class PlayerState : IServiceType, IPlayerState
public RowRef ClassJob => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->CurrentClassJobId) : default;
///
- public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default;
+ public short Level => this.IsLoaded && this.ClassJob.IsValid ? this.GetClassJobLevel(this.ClassJob.Value) : this.EffectiveLevel;
///
public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced;
diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs
index 1416dfb77..21d88010b 100644
--- a/Dalamud/Plugin/Services/IPlayerState.cs
+++ b/Dalamud/Plugin/Services/IPlayerState.cs
@@ -79,7 +79,7 @@ public interface IPlayerState : IDalamudService
bool IsLevelSynced { get; }
///
- /// Gets the effective level of the local character.
+ /// Gets the effective level of the local character, taking level sync into account.
///
short EffectiveLevel { get; }
From 14e97a1a374b7968f28dc856203e9ba990c31ccd Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Mon, 1 Dec 2025 14:19:12 -0800
Subject: [PATCH 080/201] Use local variable to track pushed style state
---
Dalamud/Interface/Windowing/Window.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index 700481ce5..e90e38119 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -57,7 +57,6 @@ public abstract class Window
private bool hasError = false;
private Exception? lastError;
- private bool isErrorStylePushed;
///
/// Initializes a new instance of the class.
@@ -426,6 +425,7 @@ public abstract class Window
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
}
+ var isErrorStylePushed = false;
if (!this.hasError)
{
this.PreDraw();
@@ -434,7 +434,7 @@ public abstract class Window
else
{
Style.StyleModelV1.DalamudStandard.Push();
- this.isErrorStylePushed = true;
+ isErrorStylePushed = true;
}
if (this.ForceMainWindow)
@@ -697,7 +697,7 @@ public abstract class Window
}
else
{
- if (this.isErrorStylePushed)
+ if (isErrorStylePushed)
{
Style.StyleModelV1.DalamudStandard.Pop();
}
From 518b3a4fb351eb1a826d3712f1c7fc4b712d658f Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Wed, 3 Dec 2025 16:43:12 +0100
Subject: [PATCH 081/201] Fix NounProcessor BeastTribe column offset
---
Dalamud/Game/Text/Noun/NounParams.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Game/Text/Noun/NounParams.cs b/Dalamud/Game/Text/Noun/NounParams.cs
index 3d5c424be..ab7a732d2 100644
--- a/Dalamud/Game/Text/Noun/NounParams.cs
+++ b/Dalamud/Game/Text/Noun/NounParams.cs
@@ -60,8 +60,8 @@ internal record struct NounParams()
///
public readonly int ColumnOffset => this.SheetName switch
{
- // See "E8 ?? ?? ?? ?? 44 8B 6B 08"
- nameof(LSheets.BeastTribe) => 10,
+ // See "E8 ?? ?? ?? ?? 44 8B 66 ?? 8B E8"
+ nameof(LSheets.BeastTribe) => 11,
nameof(LSheets.DeepDungeonItem) => 1,
nameof(LSheets.DeepDungeonEquipment) => 1,
nameof(LSheets.DeepDungeonMagicStone) => 1,
From f198ce46dc1d2100d95d4c090e446077e2af5729 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Wed, 3 Dec 2025 16:47:13 +0100
Subject: [PATCH 082/201] Add self tests for ColumnOffset
---
.../Steps/NounProcessorSelfTestStep.cs | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs
index ccb23d395..ccccc691c 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs
@@ -191,6 +191,29 @@ internal class NounProcessorSelfTestStep : ISelfTestStep
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"),
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"),
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"),
+
+ // ColumnOffset tests
+
+ new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a Amalj'aa"),
+ new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Amalj'aa"),
+
+ new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an aetherpool arm"),
+ new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the aetherpool arm"),
+
+ new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pomander of safety"),
+ new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pomander of safety"),
+
+ new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a splinter of Inferno magicite"),
+ new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the splinter of Inferno magicite"),
+
+ new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an Unei demiclone"),
+ new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Unei demiclone"),
+
+ new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pair of oval spectacles"),
+ new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pair of oval spectacles"),
+
+ new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a shaded spectacles"),
+ new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the shaded spectacles"),
];
private enum GermanCases
From 0e6dae9f6476050eaca0e01d2560bbbb136b123d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 3 Dec 2025 18:39:04 +0000
Subject: [PATCH 083/201] Update ClientStructs
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index e5f586630..e5dedba42 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77
+Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f
From df0bfc18c3877c027000b5400e78359e8b21b9f0 Mon Sep 17 00:00:00 2001
From: goat <16760685+goaaats@users.noreply.github.com>
Date: Thu, 4 Dec 2025 01:10:51 +0100
Subject: [PATCH 084/201] Make ImGuiHelpers.CreateDrawData() internal for now
---
Dalamud/Interface/Utility/ImGuiHelpers.cs | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs
index b8e7d5fe3..ee2840d3d 100644
--- a/Dalamud/Interface/Utility/ImGuiHelpers.cs
+++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs
@@ -234,15 +234,6 @@ public static partial class ImGuiHelpers
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
Service.Get().CompileAndDrawWrapped(text, style, imGuiId, buttonFlags);
- /// Creates a draw data that will draw the given SeString onto it.
- /// SeString to render.
- /// Initial rendering style.
- /// A new self-contained draw data.
- public static BufferBackedImDrawData CreateDrawData(
- ReadOnlySpan sss,
- scoped in SeStringDrawParams style = default) =>
- Service.Get().CreateDrawData(sss, style);
-
///
/// Write unformatted text wrapped.
///
@@ -584,6 +575,15 @@ public static partial class ImGuiHelpers
public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) =>
self.IsNull ? other : self;
+ /// Creates a draw data that will draw the given SeString onto it.
+ /// SeString to render.
+ /// Initial rendering style.
+ /// A new self-contained draw data.
+ internal static BufferBackedImDrawData CreateDrawData(
+ ReadOnlySpan sss,
+ scoped in SeStringDrawParams style = default) =>
+ Service.Get().CreateDrawData(sss, style);
+
///
/// Mark 4K page as used, after adding a codepoint to a font.
///
From 1fe2d5412839378a446fdf2aea867eb66d731c78 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Thu, 4 Dec 2025 01:29:04 +0100
Subject: [PATCH 085/201] Upgrade cimgui, prep for viewport alpha
---
lib/cimgui | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/cimgui b/lib/cimgui
index 27c8565f6..bc3272967 160000
--- a/lib/cimgui
+++ b/lib/cimgui
@@ -1 +1 @@
-Subproject commit 27c8565f631b004c3266373890e41ecc627f775b
+Subproject commit bc327296758d57d3bdc963cb6ce71dd5b0c7e54c
From 9bce0d33a6deb5b1c8ca22cd53389a97c459dfdf Mon Sep 17 00:00:00 2001
From: goaaats
Date: Thu, 4 Dec 2025 02:04:27 +0100
Subject: [PATCH 086/201] Don't try to free CLR memory
---
.../ImGuiSeStringRenderer/Internal/SeStringRenderer.cs | 2 +-
.../Interface/ImGuiSeStringRenderer/SeStringDrawState.cs | 6 +-----
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
index 0099e6e5d..87df2da2c 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
@@ -168,7 +168,7 @@ internal class SeStringRenderer : IServiceType
// This also does argument validation for drawParams. Do it here.
// `using var` makes a struct read-only, but we do want to modify it.
- using var stateStorage = new SeStringDrawState(
+ var stateStorage = new SeStringDrawState(
sss,
drawParams,
ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes),
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
index 722de1fda..11c1120b4 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -17,7 +17,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer;
/// Calculated values from using ImGui styles.
[StructLayout(LayoutKind.Sequential)]
-public unsafe ref struct SeStringDrawState : IDisposable
+public unsafe ref struct SeStringDrawState
{
private static readonly int ChannelCount = Enum.GetValues().Length;
@@ -181,10 +181,6 @@ public unsafe ref struct SeStringDrawState : IDisposable
/// Gets the text fragments.
internal List Fragments { get; }
- ///
- public void Dispose() =>
- ImGuiNative.Destroy((ImDrawListSplitter*)Unsafe.AsPointer(ref this.splitter));
-
/// Sets the current channel in the ImGui draw list splitter.
/// Channel to switch to.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
From 1b5fbaa82ed43343f73dfcf72a5941ba0ca16d60 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Thu, 4 Dec 2025 02:04:45 +0100
Subject: [PATCH 087/201] Access custom font atlas fields directly through
bindings
---
.../Utility/BufferBackedImDrawData.cs | 24 ++-----------------
1 file changed, 2 insertions(+), 22 deletions(-)
diff --git a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
index 112fda8a8..e6128992a 100644
--- a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
+++ b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
@@ -39,9 +39,8 @@ public unsafe struct BufferBackedImDrawData : IDisposable
*ds = default;
var atlas = ImGui.GetIO().Fonts;
- ref var atlasTail = ref ImFontAtlasTailReal.From(atlas);
ds->SharedData = *ImGui.GetDrawListSharedData().Handle;
- ds->SharedData.TexIdCommon = atlas.Textures[atlasTail.TextureIndexCommon].TexID;
+ ds->SharedData.TexIdCommon = atlas.Textures[atlas.TextureIndexCommon].TexID;
ds->SharedData.TexUvWhitePixel = atlas.TexUvWhitePixel;
ds->SharedData.TexUvLines = (Vector4*)Unsafe.AsPointer(ref atlas.TexUvLines[0]);
ds->SharedData.Font = ImGui.GetIO().FontDefault;
@@ -60,7 +59,7 @@ public unsafe struct BufferBackedImDrawData : IDisposable
res.ListPtr._ResetForNewFrame();
res.ListPtr.PushClipRectFullScreen();
- res.ListPtr.PushTextureID(new(atlasTail.TextureIndexCommon));
+ res.ListPtr.PushTextureID(new(atlas.TextureIndexCommon));
return res;
}
@@ -90,23 +89,4 @@ public unsafe struct BufferBackedImDrawData : IDisposable
public ImDrawList List;
public ImDrawListSharedData SharedData;
}
-
- [StructLayout(LayoutKind.Sequential)]
- private struct ImFontAtlasTailReal
- {
- /// Index of texture containing the below.
- public int TextureIndexCommon;
-
- /// Custom texture rectangle ID for both of the below.
- public int PackIdCommon;
-
- /// Custom texture rectangle for white pixel and mouse cursors.
- public ImFontAtlasCustomRect RectMouseCursors;
-
- /// Custom texture rectangle for baked anti-aliased lines.
- public ImFontAtlasCustomRect RectLines;
-
- public static ref ImFontAtlasTailReal From(ImFontAtlasPtr fontAtlasPtr) =>
- ref *(ImFontAtlasTailReal*)(&fontAtlasPtr.Handle->FontBuilderFlags + sizeof(uint));
- }
}
From ddc743aae1337f968223ce791a3720d48b0e71b8 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Thu, 4 Dec 2025 23:00:36 +0100
Subject: [PATCH 088/201] Note that font ptr must be supplied when setting
TargetDrawList
---
.../Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
index f3d4c44e9..1d8126f3b 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
@@ -12,7 +12,10 @@ public record struct SeStringDrawParams
/// Gets or sets the target draw list.
/// Target draw list, default(ImDrawListPtr) to not draw, or null to use
/// (the default).
- /// If this value is set, will not be called, and ImGui ID will be ignored.
+ ///
+ /// If this value is set, will not be called, and ImGui ID will be ignored.
+ /// You must specify a valid draw list and a valid font via if you set this value,
+ /// since the renderer will not be able to retrieve them from ImGui context.
///
public ImDrawListPtr? TargetDrawList { get; set; }
From 0112e17fdb052d0e3ebd6dc87b9b8bfaeaf9e1e0 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 4 Dec 2025 23:27:06 +0100
Subject: [PATCH 089/201] Replace internal SharpDX usage with TerraFX
---
.../Internals/FontAtlasFactory.BuildToolkit.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
index 2a93cf093..41c87fd39 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
@@ -15,7 +15,6 @@ using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
-using SharpDX.DXGI;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
@@ -749,7 +748,7 @@ internal sealed partial class FontAtlasFactory
new(
width,
height,
- (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm),
+ (int)(use4 ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM),
width * bpp),
buf,
name);
From da7be64fdf3bfd69cd69d77b98d1a7eaf2f3a73a Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 4 Dec 2025 23:31:31 +0100
Subject: [PATCH 090/201] Remove SharpDX
---
Dalamud/Dalamud.csproj | 2 -
Dalamud/Interface/UiBuilder.cs | 16 ------
Dalamud/Storage/Assets/DalamudAssetPurpose.cs | 6 +--
Dalamud/Utility/VectorExtensions.cs | 51 -------------------
Directory.Packages.props | 2 -
5 files changed, 3 insertions(+), 74 deletions(-)
delete mode 100644 Dalamud/Utility/VectorExtensions.cs
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index b9b453f89..e8c2516af 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -73,8 +73,6 @@
all
-
-
diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs
index e38537018..6e4740b22 100644
--- a/Dalamud/Interface/UiBuilder.cs
+++ b/Dalamud/Interface/UiBuilder.cs
@@ -12,7 +12,6 @@ using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
-using Dalamud.Plugin;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using Serilog;
@@ -150,13 +149,6 @@ public interface IUiBuilder
///
public ImFontPtr FontMono { get; }
- ///
- /// Gets the game's active Direct3D device.
- ///
- // TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud.
- [Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")]
- SharpDX.Direct3D11.Device Device { get; }
-
/// Gets the game's active Direct3D device.
/// Pointer to the instance of IUnknown that the game is using and should be containing an ID3D11Device,
/// or 0 if it is not available yet.
@@ -302,8 +294,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
private IFontHandle? monoFontHandle;
private IFontHandle? iconFontFixedWidthHandle;
- private SharpDX.Direct3D11.Device? sdxDevice;
-
///
/// Initializes a new instance of the class and registers it.
/// You do not have to call this manually.
@@ -493,12 +483,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
this.InterfaceManagerWithScene?.MonoFontHandle
?? throw new InvalidOperationException("Scene is not yet ready.")));
- ///
- // TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud.
- [Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")]
- public SharpDX.Direct3D11.Device Device =>
- this.sdxDevice ??= new(this.InterfaceManagerWithScene!.Backend!.DeviceHandle);
-
///
public nint DeviceHandle => this.InterfaceManagerWithScene?.Backend?.DeviceHandle ?? 0;
diff --git a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs
index e6c7bd920..69de1f871 100644
--- a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs
+++ b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs
@@ -11,12 +11,12 @@ public enum DalamudAssetPurpose
Empty = 0,
///
- /// The asset is a .png file, and can be purposed as a .
+ /// The asset is a .png file, and can be purposed as a .
///
TextureFromPng = 10,
-
+
///
- /// The asset is a raw texture, and can be purposed as a .
+ /// The asset is a raw texture, and can be purposed as a .
///
TextureFromRaw = 1001,
diff --git a/Dalamud/Utility/VectorExtensions.cs b/Dalamud/Utility/VectorExtensions.cs
deleted file mode 100644
index f617c8420..000000000
--- a/Dalamud/Utility/VectorExtensions.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System.Numerics;
-
-namespace Dalamud.Utility;
-
-///
-/// Extension methods for System.Numerics.VectorN and SharpDX.VectorN.
-///
-public static class VectorExtensions
-{
- ///
- /// Converts a SharpDX vector to System.Numerics.
- ///
- /// Vector to convert.
- /// A converted vector.
- public static Vector2 ToSystem(this SharpDX.Vector2 vec) => new(x: vec.X, y: vec.Y);
-
- ///
- /// Converts a SharpDX vector to System.Numerics.
- ///
- /// Vector to convert.
- /// A converted vector.
- public static Vector3 ToSystem(this SharpDX.Vector3 vec) => new(x: vec.X, y: vec.Y, z: vec.Z);
-
- ///
- /// Converts a SharpDX vector to System.Numerics.
- ///
- /// Vector to convert.
- /// A converted vector.
- public static Vector4 ToSystem(this SharpDX.Vector4 vec) => new(x: vec.X, y: vec.Y, z: vec.Z, w: vec.W);
-
- ///
- /// Converts a System.Numerics vector to SharpDX.
- ///
- /// Vector to convert.
- /// A converted vector.
- public static SharpDX.Vector2 ToSharpDX(this Vector2 vec) => new(x: vec.X, y: vec.Y);
-
- ///
- /// Converts a System.Numerics vector to SharpDX.
- ///
- /// Vector to convert.
- /// A converted vector.
- public static SharpDX.Vector3 ToSharpDX(this Vector3 vec) => new(x: vec.X, y: vec.Y, z: vec.Z);
-
- ///
- /// Converts a System.Numerics vector to SharpDX.
- ///
- /// Vector to convert.
- /// A converted vector.
- public static SharpDX.Vector4 ToSharpDX(this Vector4 vec) => new(x: vec.X, y: vec.Y, z: vec.Z, w: vec.W);
-}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 903a8ee88..481e7591d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -27,8 +27,6 @@
-
-
From ddc31132444f350a8bb1f794a8d5e91bec00c9a3 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Fri, 5 Dec 2025 00:37:25 +0100
Subject: [PATCH 091/201] Update TerraFX.Interop.Windows
---
.../IObjectWithLocalizableName.cs | 4 +--
.../FontIdentifier/SystemFontFamilyId.cs | 4 +--
.../Interface/FontIdentifier/SystemFontId.cs | 6 ++--
.../ImGuiBackend/Helpers/ReShadePeeler.cs | 14 ++++----
.../InputHandler/Win32InputHandler.cs | 10 +++---
.../Interface/Internal/InterfaceManager.cs | 2 +-
.../ReShadeAddonInterface.Exports.cs | 6 ++--
.../ReShadeHandling/ReShadeUnwrapper.cs | 2 +-
.../Interface/Internal/StaThreadService.cs | 8 ++---
.../Textures/Internal/BitmapCodecInfo.cs | 4 +--
.../Internal/TextureManager.BlameTracker.cs | 6 ++--
.../Internal/TextureManager.Clipboard.cs | 6 ++--
Dalamud/Service/LoadingDialog.cs | 32 +++++++++----------
Dalamud/Utility/ClipboardFormats.cs | 4 +--
Dalamud/Utility/TerraFxCom/ManagedIStream.cs | 28 ++++++++--------
.../TerraFxComInterfaceExtensions.cs | 8 ++---
Directory.Packages.props | 2 +-
17 files changed, 73 insertions(+), 73 deletions(-)
diff --git a/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs
index 2b970a5fd..4b3860431 100644
--- a/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs
+++ b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs
@@ -64,9 +64,9 @@ public interface IObjectWithLocalizableName
var result = new Dictionary((int)count);
for (var i = 0u; i < count; i++)
{
- fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError();
+ fn->GetLocaleName(i, buf, maxStrLen).ThrowOnError();
var key = new string(buf);
- fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError();
+ fn->GetString(i, buf, maxStrLen).ThrowOnError();
var value = new string(buf);
result[key.ToLowerInvariant()] = value;
}
diff --git a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs
index 420ee77a4..83a5e810d 100644
--- a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs
+++ b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs
@@ -133,8 +133,8 @@ public sealed class SystemFontFamilyId : IFontFamilyId
var familyIndex = 0u;
BOOL exists = false;
- fixed (void* pName = this.EnglishName)
- sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError();
+ fixed (char* pName = this.EnglishName)
+ sfc.Get()->FindFamilyName(pName, &familyIndex, &exists).ThrowOnError();
if (!exists)
throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found.");
diff --git a/Dalamud/Interface/FontIdentifier/SystemFontId.cs b/Dalamud/Interface/FontIdentifier/SystemFontId.cs
index e11759a88..8401f4c79 100644
--- a/Dalamud/Interface/FontIdentifier/SystemFontId.cs
+++ b/Dalamud/Interface/FontIdentifier/SystemFontId.cs
@@ -113,8 +113,8 @@ public sealed class SystemFontId : IFontId
var familyIndex = 0u;
BOOL exists = false;
- fixed (void* name = this.Family.EnglishName)
- sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError();
+ fixed (char* name = this.Family.EnglishName)
+ sfc.Get()->FindFamilyName(name, &familyIndex, &exists).ThrowOnError();
if (!exists)
throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found.");
@@ -151,7 +151,7 @@ public sealed class SystemFontId : IFontId
flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError();
var path = stackalloc char[(int)pathSize + 1];
- flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError();
+ flocal.Get()->GetFilePathFromKey(refKey, refKeySize, path, pathSize + 1).ThrowOnError();
return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex());
}
diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs
index 824ba382a..3f3c98c26 100644
--- a/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs
+++ b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs
@@ -104,19 +104,19 @@ internal static unsafe class ReShadePeeler
fixed (byte* pfn5 = "glBegin"u8)
fixed (byte* pfn6 = "vkCreateDevice"u8)
{
- if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == 0)
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == null)
continue;
- if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == 0)
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == null)
continue;
- if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == 0)
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == null)
continue;
- if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == 0)
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == null)
continue;
- if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == 0)
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == null)
continue;
- if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == 0)
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == null)
continue;
- if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == 0)
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == null)
continue;
}
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
index 596df4c67..18330d3a2 100644
--- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
@@ -622,7 +622,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND),
lpfnWndProc = (delegate* unmanaged)Marshal
.GetFunctionPointerForDelegate(this.input.wndProcDelegate),
- lpszClassName = (ushort*)windowClassNamePtr,
+ lpszClassName = windowClassNamePtr,
};
if (RegisterClassExW(&wcex) == 0)
@@ -658,7 +658,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
fixed (char* windowClassNamePtr = WindowClassName)
{
UnregisterClassW(
- (ushort*)windowClassNamePtr,
+ windowClassNamePtr,
(HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module));
}
@@ -781,8 +781,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{
data->Hwnd = CreateWindowExW(
(uint)data->DwExStyle,
- (ushort*)windowClassNamePtr,
- (ushort*)windowClassNamePtr,
+ windowClassNamePtr,
+ windowClassNamePtr,
(uint)data->DwStyle,
rect.left,
rect.top,
@@ -993,7 +993,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
fixed (char* pwszTitle = MemoryHelper.ReadStringNullTerminated((nint)title))
- SetWindowTextW(data->Hwnd, (ushort*)pwszTitle);
+ SetWindowTextW(data->Hwnd, pwszTitle);
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 76a1b5172..96fcb7dfd 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -256,7 +256,7 @@ internal partial class InterfaceManager : IInternalDisposableService
var gwh = default(HWND);
fixed (char* pClass = "FFXIVGAME")
{
- while ((gwh = FindWindowExW(default, gwh, (ushort*)pClass, default)) != default)
+ while ((gwh = FindWindowExW(default, gwh, pClass, default)) != default)
{
uint pid;
_ = GetWindowThreadProcessId(gwh, &pid);
diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs
index d8d210076..d7d3b56c3 100644
--- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs
+++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs
@@ -63,11 +63,11 @@ internal sealed unsafe partial class ReShadeAddonInterface
return;
- bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res)
+ static bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res)
{
Span name8 = stackalloc byte[Encoding.UTF8.GetByteCount(name) + 1];
name8[Encoding.UTF8.GetBytes(name, name8)] = 0;
- *(nint*)res = GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0]));
+ *(nint*)res = (nint)GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0]));
return *(nint*)res != 0;
}
}
@@ -174,7 +174,7 @@ internal sealed unsafe partial class ReShadeAddonInterface
CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE,
CERT.CERT_NAME_ISSUER_FLAG,
null,
- (ushort*)Unsafe.AsPointer(ref issuerName[0]),
+ (char*)Unsafe.AsPointer(ref issuerName[0]),
pcb);
if (pcb == 0)
throw new Win32Exception("CertGetNameStringW(2)");
diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
index f1210425d..711de6eb2 100644
--- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
+++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
@@ -94,7 +94,7 @@ internal static unsafe class ReShadeUnwrapper
static bool HasProcExported(ProcessModule m, ReadOnlySpan name)
{
fixed (byte* p = name)
- return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != 0;
+ return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != null;
}
}
diff --git a/Dalamud/Interface/Internal/StaThreadService.cs b/Dalamud/Interface/Internal/StaThreadService.cs
index 87e003288..bb5caa281 100644
--- a/Dalamud/Interface/Internal/StaThreadService.cs
+++ b/Dalamud/Interface/Internal/StaThreadService.cs
@@ -216,7 +216,7 @@ internal partial class StaThreadService : IInternalDisposableService
lpfnWndProc = &MessageReceiverWndProcStatic,
hInstance = hInstance,
hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1),
- lpszClassName = (ushort*)name,
+ lpszClassName = name,
};
wndClassAtom = RegisterClassExW(&wndClass);
@@ -226,8 +226,8 @@ internal partial class StaThreadService : IInternalDisposableService
this.messageReceiverHwndTask.SetResult(
CreateWindowExW(
0,
- (ushort*)wndClassAtom,
- (ushort*)name,
+ (char*)wndClassAtom,
+ name,
0,
CW_USEDEFAULT,
CW_USEDEFAULT,
@@ -275,7 +275,7 @@ internal partial class StaThreadService : IInternalDisposableService
_ = OleFlushClipboard();
OleUninitialize();
if (wndClassAtom != 0)
- UnregisterClassW((ushort*)wndClassAtom, hInstance);
+ UnregisterClassW((char*)wndClassAtom, hInstance);
this.messageReceiverHwndTask.TrySetException(e);
}
}
diff --git a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs
index 3d5456500..ec56caadd 100644
--- a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs
+++ b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs
@@ -44,12 +44,12 @@ internal sealed class BitmapCodecInfo : IBitmapCodecInfo
private static unsafe string ReadStringUsing(
IWICBitmapCodecInfo* codecInfo,
- delegate* unmanaged readFuncPtr)
+ delegate* unmanaged[MemberFunction] readFuncPtr)
{
var cch = 0u;
_ = readFuncPtr(codecInfo, 0, null, &cch);
var buf = stackalloc char[(int)cch + 1];
- Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, (ushort*)buf, &cch));
+ Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, buf, &cch));
return new(buf, 0, (int)cch);
}
}
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs
index 837b41271..fde40d462 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs
@@ -219,14 +219,14 @@ internal sealed partial class TextureManager
return;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int QueryInterfaceStatic(IUnknown* pThis, Guid* riid, void** ppvObject) =>
ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static uint AddRefStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0);
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static uint ReleaseStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0);
}
diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs
index 8a510e967..75f7ab975 100644
--- a/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs
+++ b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs
@@ -133,7 +133,7 @@ internal sealed partial class TextureManager
},
},
};
- namea.AsSpan().CopyTo(new(fgda.fgd.e0.cFileName, 260));
+ namea.AsSpan().CopyTo(new(Unsafe.AsPointer(ref fgda.fgd.e0.cFileName[0]), 260));
AddToDataObject(
pdo,
@@ -157,7 +157,7 @@ internal sealed partial class TextureManager
},
},
};
- preferredFileNameWithoutExtension.AsSpan().CopyTo(new(fgdw.fgd.e0.cFileName, 260));
+ preferredFileNameWithoutExtension.AsSpan().CopyTo(new(Unsafe.AsPointer(ref fgdw.fgd.e0.cFileName[0]), 260));
AddToDataObject(
pdo,
@@ -450,7 +450,7 @@ internal sealed partial class TextureManager
try
{
IStream* pfs;
- SHCreateStreamOnFileW((ushort*)pPath, sharedRead, &pfs).ThrowOnError();
+ SHCreateStreamOnFileW((char*)pPath, sharedRead, &pfs).ThrowOnError();
var stgm2 = new STGMEDIUM
{
diff --git a/Dalamud/Service/LoadingDialog.cs b/Dalamud/Service/LoadingDialog.cs
index 424087743..ea45d3bb2 100644
--- a/Dalamud/Service/LoadingDialog.cs
+++ b/Dalamud/Service/LoadingDialog.cs
@@ -1,4 +1,4 @@
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
@@ -294,18 +294,18 @@ internal sealed class LoadingDialog
? null
: Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe"));
- fixed (void* pszEmpty = "-")
- fixed (void* pszWindowTitle = "Dalamud")
- fixed (void* pszDalamudBoot = "Dalamud.Boot.dll")
- fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES")
- fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide"))
- fixed (void* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs"))
- fixed (void* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs"))
+ fixed (char* pszEmpty = "-")
+ fixed (char* pszWindowTitle = "Dalamud")
+ fixed (char* pszDalamudBoot = "Dalamud.Boot.dll")
+ fixed (char* pszThemesManifestResourceName = "RT_MANIFEST_THEMES")
+ fixed (char* pszHide = Loc.Localize("LoadingDialogHide", "Hide"))
+ fixed (char* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs"))
+ fixed (char* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs"))
{
var taskDialogButton = new TASKDIALOG_BUTTON
{
nButtonID = IDOK,
- pszButtonText = (ushort*)pszHide,
+ pszButtonText = pszHide,
};
var taskDialogConfig = new TASKDIALOGCONFIG
{
@@ -318,8 +318,8 @@ internal sealed class LoadingDialog
(int)TDF_CALLBACK_TIMER |
(extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN),
dwCommonButtons = 0,
- pszWindowTitle = (ushort*)pszWindowTitle,
- pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (ushort*)extractedIcon.Handle,
+ pszWindowTitle = pszWindowTitle,
+ pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (char*)extractedIcon.Handle,
pszMainInstruction = null,
pszContent = null,
cButtons = 1,
@@ -329,9 +329,9 @@ internal sealed class LoadingDialog
pRadioButtons = null,
nDefaultRadioButton = 0,
pszVerificationText = null,
- pszExpandedInformation = (ushort*)pszEmpty,
- pszExpandedControlText = (ushort*)pszShowLatestLogs,
- pszCollapsedControlText = (ushort*)pszHideLatestLogs,
+ pszExpandedInformation = pszEmpty,
+ pszExpandedControlText = pszShowLatestLogs,
+ pszCollapsedControlText = pszHideLatestLogs,
pszFooterIcon = null,
pszFooter = null,
pfCallback = &HResultFuncBinder,
@@ -348,8 +348,8 @@ internal sealed class LoadingDialog
{
cbSize = (uint)sizeof(ACTCTXW),
dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID,
- lpResourceName = (ushort*)pszThemesManifestResourceName,
- hModule = GetModuleHandleW((ushort*)pszDalamudBoot),
+ lpResourceName = pszThemesManifestResourceName,
+ hModule = GetModuleHandleW(pszDalamudBoot),
};
hActCtx = CreateActCtxW(&actctx);
if (hActCtx == default)
diff --git a/Dalamud/Utility/ClipboardFormats.cs b/Dalamud/Utility/ClipboardFormats.cs
index 07b6c00d6..b80e05dd3 100644
--- a/Dalamud/Utility/ClipboardFormats.cs
+++ b/Dalamud/Utility/ClipboardFormats.cs
@@ -30,8 +30,8 @@ internal static class ClipboardFormats
private static unsafe uint ClipboardFormatFromName(ReadOnlySpan name)
{
uint cf;
- fixed (void* p = name)
- cf = RegisterClipboardFormatW((ushort*)p);
+ fixed (char* p = name)
+ cf = RegisterClipboardFormatW(p);
if (cf != 0)
return cf;
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ??
diff --git a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs
index caec65da2..eb1997daf 100644
--- a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs
+++ b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs
@@ -57,60 +57,60 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable
static ManagedIStream? ToManagedObject(void* pThis) =>
GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as ManagedIStream;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) =>
ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static uint AddRefStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0);
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static uint ReleaseStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0);
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) =>
ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) =>
ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int SeekStatic(
IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) =>
ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) =>
ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int CopyToStatic(
IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead,
ULARGE_INTEGER* pcbWritten) =>
ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int CommitStatic(IStream* pThis, uint grfCommitFlags) =>
ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) =>
ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int UnlockRegionStatic(
IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) =>
ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) =>
ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_UNEXPECTED;
- [UnmanagedCallersOnly]
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_UNEXPECTED;
}
diff --git a/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs
index f9252839f..ec108403e 100644
--- a/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs
+++ b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs
@@ -88,7 +88,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions
fixed (char* pPath = path)
{
SHCreateStreamOnFileEx(
- (ushort*)pPath,
+ pPath,
grfMode,
(uint)attributes,
fCreate,
@@ -115,7 +115,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions
{
fixed (char* pName = name)
{
- var option = new PROPBAG2 { pstrName = (ushort*)pName };
+ var option = new PROPBAG2 { pstrName = pName };
return obj.Write(1, &option, &varValue);
}
}
@@ -145,7 +145,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions
try
{
fixed (char* pName = name)
- return obj.SetMetadataByName((ushort*)pName, &propVarValue);
+ return obj.SetMetadataByName(pName, &propVarValue);
}
finally
{
@@ -165,7 +165,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions
public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name)
{
fixed (char* pName = name)
- return obj.RemoveMetadataByName((ushort*)pName);
+ return obj.RemoveMetadataByName(pName);
}
[LibraryImport("propsys.dll")]
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 903a8ee88..d62d247c3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -26,7 +26,7 @@
-
+
From fc983458fa16c698977b386fdefac4d385256f01 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Fri, 5 Dec 2025 01:44:18 +0100
Subject: [PATCH 092/201] Update Nuke
---
build/DalamudBuild.cs | 6 ++----
build/build.csproj | 2 +-
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs
index ba2b09a4d..1a189f2c7 100644
--- a/build/DalamudBuild.cs
+++ b/build/DalamudBuild.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.IO;
using Nuke.Common;
using Nuke.Common.Execution;
using Nuke.Common.Git;
@@ -128,7 +127,7 @@ public class DalamudBuild : NukeBuild
if (IsCIBuild)
{
s = s
- .SetProcessArgumentConfigurator(a => a.Add("/clp:NoSummary")); // Disable MSBuild summary on CI builds
+ .SetProcessAdditionalArguments("/clp:NoSummary"); // Disable MSBuild summary on CI builds
}
// We need to emit compiler generated files for the docs build, since docfx can't run generators directly
// TODO: This fails every build after this because of redefinitions...
@@ -238,7 +237,6 @@ public class DalamudBuild : NukeBuild
.SetProject(InjectorProjectFile)
.SetConfiguration(Configuration));
- FileSystemTasks.DeleteDirectory(ArtifactsDirectory);
- Directory.CreateDirectory(ArtifactsDirectory);
+ ArtifactsDirectory.CreateOrCleanDirectory();
});
}
diff --git a/build/build.csproj b/build/build.csproj
index 1e1416d92..7096c7f8a 100644
--- a/build/build.csproj
+++ b/build/build.csproj
@@ -11,7 +11,7 @@
false
-
+
From e7d4786a1fec6411908ed9e319f1a06b67738389 Mon Sep 17 00:00:00 2001
From: goat <16760685+goaaats@users.noreply.github.com>
Date: Fri, 5 Dec 2025 18:18:57 +0100
Subject: [PATCH 093/201] Oops, wrong version
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 481e7591d..6c5070d35 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -26,7 +26,7 @@
-
+
From 7cf20fe102bf14bea99dead9ec0d4e5ad3c23c92 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Fri, 5 Dec 2025 00:35:52 +0100
Subject: [PATCH 094/201] Update Microsoft.Windows.CsWin32
---
Dalamud/SafeMemory.cs | 15 +++++++++++++--
Dalamud/Utility/FilesystemUtil.cs | 10 +++++++---
Directory.Packages.props | 2 +-
3 files changed, 21 insertions(+), 6 deletions(-)
diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs
index a8ac40a5d..9a1af0625 100644
--- a/Dalamud/SafeMemory.cs
+++ b/Dalamud/SafeMemory.cs
@@ -1,6 +1,8 @@
using System.Runtime.InteropServices;
using System.Text;
+using Windows.Win32.Foundation;
+
namespace Dalamud;
///
@@ -28,12 +30,18 @@ public static class SafeMemory
/// Whether the read succeeded.
public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer)
{
+ if (Handle.IsClosed || Handle.IsInvalid)
+ {
+ buffer = [];
+ return false;
+ }
+
buffer = new byte[count <= 0 ? 0 : count];
fixed (byte* p = buffer)
{
UIntPtr bytesRead;
if (!Windows.Win32.PInvoke.ReadProcessMemory(
- Handle,
+ (HANDLE)Handle.DangerousGetHandle(),
address.ToPointer(),
p,
new UIntPtr((uint)count),
@@ -54,6 +62,9 @@ public static class SafeMemory
/// Whether the write succeeded.
public static unsafe bool WriteBytes(IntPtr address, byte[] buffer)
{
+ if (Handle.IsClosed || Handle.IsInvalid)
+ return false;
+
if (buffer.Length == 0)
return true;
@@ -61,7 +72,7 @@ public static class SafeMemory
fixed (byte* p = buffer)
{
if (!Windows.Win32.PInvoke.WriteProcessMemory(
- Handle,
+ (HANDLE)Handle.DangerousGetHandle(),
address.ToPointer(),
p,
new UIntPtr((uint)buffer.Length),
diff --git a/Dalamud/Utility/FilesystemUtil.cs b/Dalamud/Utility/FilesystemUtil.cs
index 3b4298b37..560e06da3 100644
--- a/Dalamud/Utility/FilesystemUtil.cs
+++ b/Dalamud/Utility/FilesystemUtil.cs
@@ -1,7 +1,8 @@
-using System.ComponentModel;
+using System.ComponentModel;
using System.IO;
using System.Text;
+using Windows.Win32.Foundation;
using Windows.Win32.Storage.FileSystem;
namespace Dalamud.Utility;
@@ -61,8 +62,11 @@ public static class FilesystemUtil
// Write the data
uint bytesWritten = 0;
- if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan(bytes), &bytesWritten, null))
- throw new Win32Exception();
+ fixed (byte* ptr = bytes)
+ {
+ if (!Windows.Win32.PInvoke.WriteFile((HANDLE)tempFile.DangerousGetHandle(), ptr, (uint)bytes.Length, &bytesWritten, null))
+ throw new Win32Exception();
+ }
if (bytesWritten != bytes.Length)
throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})");
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6c5070d35..58e355400 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -27,7 +27,7 @@
-
+
From d94cacaac3bec7e2b64d3d13bde9d1dbd61d0714 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Fri, 5 Dec 2025 19:10:31 +0100
Subject: [PATCH 095/201] Disable SafeHandles
---
Dalamud/EntryPoint.cs | 2 +-
Dalamud/Hooking/Hook.cs | 14 +++++++-------
Dalamud/NativeMethods.json | 1 +
Dalamud/SafeMemory.cs | 12 ++++++------
Dalamud/Utility/FilesystemUtil.cs | 16 +++++++++++-----
Dalamud/Utility/Util.cs | 5 ++---
6 files changed, 28 insertions(+), 22 deletions(-)
diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs
index 15077f3d8..b5504b046 100644
--- a/Dalamud/EntryPoint.cs
+++ b/Dalamud/EntryPoint.cs
@@ -263,7 +263,7 @@ public sealed class EntryPoint
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
var searchPath = $".;{symbolPath}";
- var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle();
+ var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess();
// Remove any existing Symbol Handler and Init a new one with our search path added
Windows.Win32.PInvoke.SymCleanup(currentProcess);
diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs
index faf4658a5..1cd3ef91d 100644
--- a/Dalamud/Hooking/Hook.cs
+++ b/Dalamud/Hooking/Hook.cs
@@ -201,19 +201,19 @@ public abstract class Hook : IDalamudHook where T : Delegate
if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true;
- using var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName);
- if (moduleHandle.IsInvalid)
+ var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName);
+ if (moduleHandle.IsNull)
throw new Exception($"Could not get a handle to module {moduleName}");
- var procAddress = (nint)Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName);
- if (procAddress == IntPtr.Zero)
+ var procAddress = Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName);
+ if (procAddress.IsNull)
throw new Exception($"Could not get the address of {moduleName}::{exportName}");
- procAddress = HookManager.FollowJmp(procAddress);
+ var address = HookManager.FollowJmp(procAddress.Value);
if (useMinHook)
- return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly());
+ return new MinHookHook(address, detour, Assembly.GetCallingAssembly());
else
- return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly());
+ return new ReloadedHook(address, detour, Assembly.GetCallingAssembly());
}
///
diff --git a/Dalamud/NativeMethods.json b/Dalamud/NativeMethods.json
index ffb313dfc..46fd3504f 100644
--- a/Dalamud/NativeMethods.json
+++ b/Dalamud/NativeMethods.json
@@ -1,4 +1,5 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
+ "useSafeHandles": false,
"allowMarshaling": false
}
diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs
index 9a1af0625..ca0c8ff92 100644
--- a/Dalamud/SafeMemory.cs
+++ b/Dalamud/SafeMemory.cs
@@ -14,11 +14,11 @@ namespace Dalamud;
///
public static class SafeMemory
{
- private static readonly SafeHandle Handle;
+ private static readonly HANDLE Handle;
static SafeMemory()
{
- Handle = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle();
+ Handle = Windows.Win32.PInvoke.GetCurrentProcess();
}
///
@@ -30,7 +30,7 @@ public static class SafeMemory
/// Whether the read succeeded.
public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer)
{
- if (Handle.IsClosed || Handle.IsInvalid)
+ if (Handle.IsNull)
{
buffer = [];
return false;
@@ -41,7 +41,7 @@ public static class SafeMemory
{
UIntPtr bytesRead;
if (!Windows.Win32.PInvoke.ReadProcessMemory(
- (HANDLE)Handle.DangerousGetHandle(),
+ Handle,
address.ToPointer(),
p,
new UIntPtr((uint)count),
@@ -62,7 +62,7 @@ public static class SafeMemory
/// Whether the write succeeded.
public static unsafe bool WriteBytes(IntPtr address, byte[] buffer)
{
- if (Handle.IsClosed || Handle.IsInvalid)
+ if (Handle.IsNull)
return false;
if (buffer.Length == 0)
@@ -72,7 +72,7 @@ public static class SafeMemory
fixed (byte* p = buffer)
{
if (!Windows.Win32.PInvoke.WriteProcessMemory(
- (HANDLE)Handle.DangerousGetHandle(),
+ Handle,
address.ToPointer(),
p,
new UIntPtr((uint)buffer.Length),
diff --git a/Dalamud/Utility/FilesystemUtil.cs b/Dalamud/Utility/FilesystemUtil.cs
index 560e06da3..f1b62ee21 100644
--- a/Dalamud/Utility/FilesystemUtil.cs
+++ b/Dalamud/Utility/FilesystemUtil.cs
@@ -48,33 +48,39 @@ public static class FilesystemUtil
// Open the temp file
var tempPath = path + ".tmp";
- using var tempFile = Windows.Win32.PInvoke.CreateFile(
+ var tempFile = Windows.Win32.PInvoke.CreateFile(
tempPath,
(uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE),
FILE_SHARE_MODE.FILE_SHARE_NONE,
null,
FILE_CREATION_DISPOSITION.CREATE_ALWAYS,
FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL,
- null);
+ HANDLE.Null);
- if (tempFile.IsInvalid)
+ if (tempFile.IsNull)
throw new Win32Exception();
// Write the data
uint bytesWritten = 0;
fixed (byte* ptr = bytes)
{
- if (!Windows.Win32.PInvoke.WriteFile((HANDLE)tempFile.DangerousGetHandle(), ptr, (uint)bytes.Length, &bytesWritten, null))
+ if (!Windows.Win32.PInvoke.WriteFile(tempFile, ptr, (uint)bytes.Length, &bytesWritten, null))
throw new Win32Exception();
}
if (bytesWritten != bytes.Length)
+ {
+ Windows.Win32.PInvoke.CloseHandle(tempFile);
throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})");
+ }
if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile))
+ {
+ Windows.Win32.PInvoke.CloseHandle(tempFile);
throw new Win32Exception();
+ }
- tempFile.Close();
+ Windows.Win32.PInvoke.CloseHandle(tempFile);
if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH))
throw new Win32Exception();
diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs
index 19610ef64..f50efcf0d 100644
--- a/Dalamud/Utility/Util.cs
+++ b/Dalamud/Utility/Util.cs
@@ -858,7 +858,7 @@ public static partial class Util
var sizeWithTerminators = pathBytesSize + (pathBytes.Length * 2);
var dropFilesSize = sizeof(DROPFILES);
- var hGlobal = Win32_PInvoke.GlobalAlloc_SafeHandle(
+ var hGlobal = Win32_PInvoke.GlobalAlloc(
GLOBAL_ALLOC_FLAGS.GHND,
// struct size + size of encoded strings + null terminator for each
// string + two null terminators for end of list
@@ -896,12 +896,11 @@ public static partial class Util
{
Win32_PInvoke.SetClipboardData(
(uint)CLIPBOARD_FORMAT.CF_HDROP,
- hGlobal);
+ (Windows.Win32.Foundation.HANDLE)hGlobal.Value);
Win32_PInvoke.CloseClipboard();
return true;
}
- hGlobal.Dispose();
return false;
}
From a36e11574b14ea6887cb0f7d2513920ebdd820bf Mon Sep 17 00:00:00 2001
From: goat <16760685+goaaats@users.noreply.github.com>
Date: Sat, 6 Dec 2025 01:10:00 +0100
Subject: [PATCH 096/201] Add git status checks to workflow to see what's dirty
---
.github/workflows/main.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 299d71e95..f552e446b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -33,8 +33,12 @@ jobs:
($env:REPO_NAME) >> VERSION
($env:BRANCH) >> VERSION
($env:COMMIT) >> VERSION
+ - name: git status
+ run: git status
- name: Build and Test Dalamud
run: .\build.ps1 ci
+ - name: git status
+ run: git status
- name: Sign Dalamud
if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }}
env:
From 3c7dbf9f81e147e45f7b3541844e95cebcc3a5df Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 5 Dec 2025 16:59:17 -0800
Subject: [PATCH 097/201] Remove AddonEventManagerAddressResolver.cs
---
.../Game/Addon/Events/AddonEventManager.cs | 30 ++++++++-----------
.../AddonEventManagerAddressResolver.cs | 21 -------------
2 files changed, 12 insertions(+), 39 deletions(-)
delete mode 100644 Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs
index 945197e2b..980404940 100644
--- a/Dalamud/Game/Addon/Events/AddonEventManager.cs
+++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs
@@ -9,7 +9,6 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events;
@@ -32,25 +31,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService
private readonly AddonLifecycleEventListener finalizeEventListener;
- private readonly AddonEventManagerAddressResolver address;
- private readonly Hook onUpdateCursor;
+ private readonly Hook onUpdateCursor;
private readonly ConcurrentDictionary pluginEventControllers;
- private AddonCursorType? cursorOverride;
+ private AtkCursor.CursorType? cursorOverride;
[ServiceManager.ServiceConstructor]
- private AddonEventManager(TargetSigScanner sigScanner)
+ private AddonEventManager()
{
- this.address = new AddonEventManagerAddressResolver();
- this.address.Setup(sigScanner);
-
this.pluginEventControllers = new ConcurrentDictionary();
this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
this.cursorOverride = null;
- this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
+ this.onUpdateCursor = Hook.FromAddress(AtkUnitManager.Addresses.UpdateCursor.Value, this.UpdateCursorDetour);
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
this.addonLifecycle.RegisterListener(this.finalizeEventListener);
@@ -58,8 +53,6 @@ internal unsafe class AddonEventManager : IInternalDisposableService
this.onUpdateCursor.Enable();
}
- private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
-
///
void IInternalDisposableService.DisposeService()
{
@@ -117,7 +110,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
/// Force the game cursor to be the specified cursor.
///
/// Which cursor to use.
- internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
+ internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = (AtkCursor.CursorType)cursor;
///
/// Un-forces the game cursor.
@@ -168,7 +161,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
}
}
- private nint UpdateCursorDetour(RaptureAtkModule* module)
+ private void UpdateCursorDetour(AtkUnitManager* thisPtr)
{
try
{
@@ -176,13 +169,14 @@ internal unsafe class AddonEventManager : IInternalDisposableService
if (this.cursorOverride is not null && atkStage is not null)
{
- var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
- if (cursor != this.cursorOverride)
+ ref var atkCursor = ref atkStage->AtkCursor;
+
+ if (atkCursor.Type != this.cursorOverride)
{
- AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
+ atkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
}
- return nint.Zero;
+ return;
}
}
catch (Exception e)
@@ -190,7 +184,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
Log.Error(e, "Exception in UpdateCursorDetour.");
}
- return this.onUpdateCursor!.Original(module);
+ this.onUpdateCursor!.Original(thisPtr);
}
}
diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
deleted file mode 100644
index 415e1b169..000000000
--- a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Dalamud.Game.Addon.Events;
-
-///
-/// AddonEventManager memory address resolver.
-///
-internal class AddonEventManagerAddressResolver : BaseAddressResolver
-{
- ///
- /// Gets the address of the AtkModule UpdateCursor method.
- ///
- public nint UpdateCursor { get; private set; }
-
- ///
- /// Scan for and setup any configured address pointers.
- ///
- /// The signature scanner to facilitate setup.
- protected override void Setup64Bit(ISigScanner scanner)
- {
- this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS
- }
-}
From 45366efd9fc888bf3908144f248b46da00c5d4ff Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 5 Dec 2025 17:10:58 -0800
Subject: [PATCH 098/201] Remove SigScanner from ctor
---
Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index ddcebe718..716ce1bfb 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -29,7 +29,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
private Hook? onInitializeAddonHook;
[ServiceManager.ServiceConstructor]
- private AddonLifecycle(TargetSigScanner sigScanner)
+ private AddonLifecycle()
{
this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
this.onInitializeAddonHook.Enable();
From 2e5c560ed729b7e88e6f036aa9987bca5e6f7be0 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Sat, 6 Dec 2025 12:48:34 +0000
Subject: [PATCH 099/201] Update ClientStructs
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index e5dedba42..6f339d8f7 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f
+Subproject commit 6f339d8f725fa6922449f7e5c584ca6b8fa2fb19
From 9c2d2b7c1dbe192599748e7320fce3c1cc972072 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 6 Dec 2025 15:07:09 +0100
Subject: [PATCH 100/201] Report CLR errors through DalamudCrashHandler/VEH by
hooking ReportEventW
---
Dalamud.Boot/dllmain.cpp | 41 ++++++++++++++++++++++++++++++++++++++++
Dalamud.Boot/veh.cpp | 37 ++++++++++++++++++++++++++++--------
Dalamud.Boot/veh.h | 1 +
3 files changed, 71 insertions(+), 8 deletions(-)
diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp
index 80a16f89a..2af78092d 100644
--- a/Dalamud.Boot/dllmain.cpp
+++ b/Dalamud.Boot/dllmain.cpp
@@ -331,6 +331,47 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("VEH was disabled manually");
}
+ // ============================== CLR Reporting =================================== //
+
+ // This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it
+ // was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now.
+ // Ideally all of this will go away once they get to it.
+ static std::shared_ptr> s_hook;
+ s_hook = std::make_shared>("advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
+ s_hook->set_detour([hook = s_hook.get()]( HANDLE hEventLog,
+ WORD wType,
+ WORD wCategory,
+ DWORD dwEventID,
+ PSID lpUserSid,
+ WORD wNumStrings,
+ DWORD dwDataSize,
+ LPCWSTR* lpStrings,
+ LPVOID lpRawData)->BOOL {
+
+ // Check for CLR Error Event IDs
+ if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception
+ dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast
+ dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime
+ dwEventID != 1027 && // ERT_StackOverflow: The process was terminated due to a stack overflow
+ dwEventID != 1028) // ERT_CodeContractFailed: The application encountered a bug. A managed code contract (precondition, postcondition, object invariant, or assert) failed
+ {
+ return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
+ }
+
+ if (wNumStrings == 0 || lpStrings == nullptr) {
+ logging::W("ReportEventW called with no strings.");
+ return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
+ }
+
+ // In most cases, DalamudCrashHandler will kill us now, so call original here to make sure we still write to the event log.
+ const BOOL original_ret = hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
+
+ const std::wstring error_details(lpStrings[0]);
+ veh::raise_external_event(error_details);
+
+ return original_ret;
+ });
+
// ============================== Dalamud ==================================== //
if (static_cast(g_startInfo.BootWaitMessageBox) & static_cast(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))
diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp
index b0ec1cefa..24a846e66 100644
--- a/Dalamud.Boot/veh.cpp
+++ b/Dalamud.Boot/veh.cpp
@@ -11,6 +11,8 @@
#include "crashhandler_shared.h"
#include "DalamudStartInfo.h"
+#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
+
#pragma comment(lib, "comctl32.lib")
#if defined _M_IX86
@@ -31,6 +33,8 @@ HANDLE g_crashhandler_process = nullptr;
HANDLE g_crashhandler_event = nullptr;
HANDLE g_crashhandler_pipe_write = nullptr;
+wchar_t g_external_event_info[16384] = L"";
+
std::recursive_mutex g_exception_handler_mutex;
std::chrono::time_point g_time_start;
@@ -190,7 +194,11 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
std::wstring stackTrace;
- if (!g_clr)
+ if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
+ {
+ stackTrace = std::wstring(g_external_event_info);
+ }
+ else if (!g_clr)
{
stackTrace = L"(no CLR stack trace available)";
}
@@ -251,6 +259,12 @@ LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
{
+ // special case for CLR exceptions, always trigger crash handler
+ if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
+ {
+ return exception_handler(ex);
+ }
+
if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{
// pass
@@ -268,7 +282,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) &&
!is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip))
- return EXCEPTION_CONTINUE_SEARCH;
+ return EXCEPTION_CONTINUE_SEARCH;
}
return exception_handler(ex);
@@ -297,7 +311,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536))
{
hWritePipe.emplace(hWritePipeRaw, &CloseHandle);
-
+
if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE))
{
hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle);
@@ -315,9 +329,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
}
// additional information
- STARTUPINFOEXW siex{};
+ STARTUPINFOEXW siex{};
PROCESS_INFORMATION pi{};
-
+
siex.StartupInfo.cb = sizeof siex;
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE;
@@ -385,7 +399,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
argstr.push_back(L' ');
}
argstr.pop_back();
-
+
if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr))
{
logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError());
@@ -400,7 +414,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
TRUE, // Set handle inheritance to FALSE
EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W)
nullptr, // Use parent's environment block
- nullptr, // Use parent's starting directory
+ nullptr, // Use parent's starting directory
&siex.StartupInfo, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
))
@@ -416,7 +430,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
}
CloseHandle(pi.hThread);
-
+
g_crashhandler_process = pi.hProcess;
g_crashhandler_pipe_write = hWritePipe->release();
logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId);
@@ -434,3 +448,10 @@ bool veh::remove_handler()
}
return false;
}
+
+void veh::raise_external_event(const std::wstring& errorMessage)
+{
+ const auto info_size = std::min(errorMessage.size(), std::size(g_external_event_info) - 1);
+ wcsncpy_s(g_external_event_info, errorMessage.c_str(), info_size);
+ RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr);
+}
diff --git a/Dalamud.Boot/veh.h b/Dalamud.Boot/veh.h
index 1905272ea..2a02c374e 100644
--- a/Dalamud.Boot/veh.h
+++ b/Dalamud.Boot/veh.h
@@ -4,4 +4,5 @@ namespace veh
{
bool add_handler(bool doFullDump, const std::string& workingDirectory);
bool remove_handler();
+ void raise_external_event(const std::wstring& info);
}
From e09c43b8decc55fcbc38cd61102256db0bdc4e29 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 6 Dec 2025 15:07:46 +0100
Subject: [PATCH 101/201] Fix bad exit condition when looping exception records
---
DalamudCrashHandler/DalamudCrashHandler.cpp | 78 ++++++++++++---------
1 file changed, 43 insertions(+), 35 deletions(-)
diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp
index ec7115ffd..107261541 100644
--- a/DalamudCrashHandler/DalamudCrashHandler.cpp
+++ b/DalamudCrashHandler/DalamudCrashHandler.cpp
@@ -119,7 +119,7 @@ std::wstring describe_module(const std::filesystem::path& path) {
return std::format(L"", GetLastError());
UINT size = 0;
-
+
std::wstring version = L"v?.?.?.?";
if (LPVOID lpBuffer; VerQueryValueW(block.data(), L"\\", &lpBuffer, &size)) {
const auto& v = *static_cast(lpBuffer);
@@ -176,7 +176,7 @@ const std::map& get_remote_modules() {
std::vector buf(8192);
for (size_t i = 0; i < 64; i++) {
if (DWORD needed; !EnumProcessModules(g_hProcess, &buf[0], static_cast(std::span(buf).size_bytes()), &needed)) {
- std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl;
+ std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl;
break;
} else if (needed > std::span(buf).size_bytes()) {
buf.resize(needed / sizeof(HMODULE) + 16);
@@ -201,7 +201,7 @@ const std::map& get_remote_modules() {
data[hModule] = nth64.OptionalHeader.SizeOfImage;
}
-
+
return data;
}();
@@ -292,35 +292,43 @@ std::wstring to_address_string(const DWORD64 address, const bool try_ptrderef =
void print_exception_info(HANDLE hThread, const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log) {
std::vector exRecs;
- if (ex.ExceptionRecord) {
+ if (ex.ExceptionRecord)
+ {
size_t rec_index = 0;
size_t read;
- exRecs.emplace_back();
+
for (auto pRemoteExRec = ex.ExceptionRecord;
- pRemoteExRec
- && rec_index < 64
- && ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read)
- && read >= offsetof(EXCEPTION_RECORD, ExceptionInformation)
- && read >= static_cast(reinterpret_cast(&exRecs.back().ExceptionInformation[exRecs.back().NumberParameters]) - reinterpret_cast(&exRecs.back()));
- rec_index++) {
+ pRemoteExRec && rec_index < 64;
+ rec_index++)
+ {
+ exRecs.emplace_back();
+
+ if (!ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read)
+ || read < offsetof(EXCEPTION_RECORD, ExceptionInformation)
+ || read < static_cast(reinterpret_cast(&exRecs.back().ExceptionInformation[exRecs.
+ back().NumberParameters]) - reinterpret_cast(&exRecs.back())))
+ {
+ exRecs.pop_back();
+ break;
+ }
log << std::format(L"\nException Info #{}\n", rec_index);
log << std::format(L"Address: {:X}\n", exRecs.back().ExceptionCode);
log << std::format(L"Flags: {:X}\n", exRecs.back().ExceptionFlags);
log << std::format(L"Address: {:X}\n", reinterpret_cast(exRecs.back().ExceptionAddress));
- if (!exRecs.back().NumberParameters)
- continue;
- log << L"Parameters: ";
- for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i) {
- if (i != 0)
- log << L", ";
- log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]);
+ if (exRecs.back().NumberParameters)
+ {
+ log << L"Parameters: ";
+ for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i)
+ {
+ if (i != 0)
+ log << L", ";
+ log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]);
+ }
}
pRemoteExRec = exRecs.back().ExceptionRecord;
- exRecs.emplace_back();
}
- exRecs.pop_back();
}
log << L"\nCall Stack\n{";
@@ -410,7 +418,7 @@ void print_exception_info_extended(const EXCEPTION_POINTERS& ex, const CONTEXT&
std::wstring escape_shell_arg(const std::wstring& arg) {
// https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
-
+
std::wstring res;
if (!arg.empty() && arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) {
res.append(arg);
@@ -504,7 +512,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
filePath.emplace(pFilePath);
std::fstream fileStream(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);
-
+
mz_zip_archive zipa{};
zipa.m_pIO_opaque = &fileStream;
zipa.m_pRead = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t {
@@ -566,7 +574,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
const auto hLogFile = CreateFileW(logFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hLogFile == INVALID_HANDLE_VALUE)
throw_last_error(std::format("indiv. log file: CreateFileW({})", ws_to_u8(logFilePath.wstring())));
-
+
std::unique_ptr hLogFileClose(hLogFile, &CloseHandle);
LARGE_INTEGER size, baseOffset{};
@@ -695,7 +703,7 @@ int main() {
// IFileSaveDialog only works on STA
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
-
+
std::vector args;
if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) {
for (auto i = 0; i < argc; i++)
@@ -823,14 +831,14 @@ int main() {
hr = pOleWindow->GetWindow(&hwndProgressDialog);
if (SUCCEEDED(hr))
{
- SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0,
+ SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
SetForegroundWindow(hwndProgressDialog);
}
-
+
pOleWindow->Release();
}
-
+
}
else {
std::cerr << "Failed to create progress window" << std::endl;
@@ -852,14 +860,14 @@ int main() {
https://github.com/sumatrapdfreader/sumatrapdf/blob/master/src/utils/DbgHelpDyn.cpp
*/
-
+
if (g_bSymbolsAvailable) {
SymRefreshModuleList(g_hProcess);
}
else if(!assetDir.empty())
{
auto symbol_search_path = std::format(L".;{}", (assetDir / "UIRes" / "pdb").wstring());
-
+
g_bSymbolsAvailable = SymInitializeW(g_hProcess, symbol_search_path.c_str(), true);
std::wcout << std::format(L"Init symbols with PDB at {}", symbol_search_path) << std::endl;
@@ -870,12 +878,12 @@ int main() {
g_bSymbolsAvailable = SymInitializeW(g_hProcess, nullptr, true);
std::cout << "Init symbols without PDB" << std::endl;
}
-
+
if (!g_bSymbolsAvailable) {
std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl;
}
- if (pProgressDialog)
+ if (pProgressDialog)
pProgressDialog->SetLine(3, L"Reading troubleshooting data", FALSE, NULL);
std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0');
@@ -936,7 +944,7 @@ int main() {
if (shutup)
log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl;
-
+
if (dumpPath.empty())
log << L"Dump skipped" << std::endl;
else if (dumpError.empty())
@@ -1003,7 +1011,7 @@ int main() {
R"aa(Help | Open log directory | Open log file)aa"
);
#endif
-
+
// Can't do this, xiv stops pumping messages here
//config.hwndParent = FindWindowA("FFXIVGAME", NULL);
@@ -1056,13 +1064,13 @@ int main() {
return (*reinterpret_cast(dwRefData))(hwnd, uNotification, wParam, lParam);
};
config.lpCallbackData = reinterpret_cast(&callback);
-
+
if (pProgressDialog) {
pProgressDialog->StopProgressDialog();
pProgressDialog->Release();
pProgressDialog = NULL;
}
-
+
const auto kill_game = [&] { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); };
if (shutup) {
From 446c7e38771a943ac7b2dd9d5b48478f95d66250 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 6 Dec 2025 15:14:13 +0100
Subject: [PATCH 102/201] Some logging, cleanup
---
Dalamud.Boot/dllmain.cpp | 26 +++++++++++++++-----------
Dalamud.Boot/veh.cpp | 6 +++---
2 files changed, 18 insertions(+), 14 deletions(-)
diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp
index 2af78092d..687089f82 100644
--- a/Dalamud.Boot/dllmain.cpp
+++ b/Dalamud.Boot/dllmain.cpp
@@ -336,19 +336,22 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
// This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it
// was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now.
// Ideally all of this will go away once they get to it.
- static std::shared_ptr> s_hook;
- s_hook = std::make_shared>("advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
- s_hook->set_detour([hook = s_hook.get()]( HANDLE hEventLog,
- WORD wType,
- WORD wCategory,
- DWORD dwEventID,
- PSID lpUserSid,
- WORD wNumStrings,
- DWORD dwDataSize,
- LPCWSTR* lpStrings,
- LPVOID lpRawData)->BOOL {
+ static std::shared_ptr> s_report_event_hook;
+ s_report_event_hook = std::make_shared>(
+ "advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
+ s_report_event_hook->set_detour([hook = s_report_event_hook.get()](
+ HANDLE hEventLog,
+ WORD wType,
+ WORD wCategory,
+ DWORD dwEventID,
+ PSID lpUserSid,
+ WORD wNumStrings,
+ DWORD dwDataSize,
+ LPCWSTR* lpStrings,
+ LPVOID lpRawData)-> BOOL {
// Check for CLR Error Event IDs
+ // https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/eventreporter.cpp#L370
if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception
dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast
dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime
@@ -371,6 +374,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
return original_ret;
});
+ logging::I("ReportEventW hook installed.");
// ============================== Dalamud ==================================== //
diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp
index 24a846e66..25c9b5045 100644
--- a/Dalamud.Boot/veh.cpp
+++ b/Dalamud.Boot/veh.cpp
@@ -449,9 +449,9 @@ bool veh::remove_handler()
return false;
}
-void veh::raise_external_event(const std::wstring& errorMessage)
+void veh::raise_external_event(const std::wstring& info)
{
- const auto info_size = std::min(errorMessage.size(), std::size(g_external_event_info) - 1);
- wcsncpy_s(g_external_event_info, errorMessage.c_str(), info_size);
+ const auto info_size = std::min(info.size(), std::size(g_external_event_info) - 1);
+ wcsncpy_s(g_external_event_info, info.c_str(), info_size);
RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr);
}
From 1d1db04f04f98a353995f836c53a4e3918b683a6 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Sat, 6 Dec 2025 16:09:42 +0100
Subject: [PATCH 103/201] Use ImFontPtr in SeStringDrawState
---
.../ImGuiSeStringRenderer/SeStringDrawState.cs | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
index 11c1120b4..3a21e0db9 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -76,7 +76,7 @@ public unsafe ref struct SeStringDrawState
this.splitter = default;
this.GetEntity = ssdp.GetEntity;
this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y));
- this.FontSizeScale = this.FontSize / this.Font->FontSize;
+ this.FontSizeScale = this.FontSize / this.Font.FontSize;
this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight);
this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f;
this.Opacity = ssdp.EffectiveOpacity;
@@ -106,7 +106,7 @@ public unsafe ref struct SeStringDrawState
public Vector2 ScreenOffset { get; }
///
- public ImFont* Font { get; }
+ public ImFontPtr Font { get; }
///
public float FontSize { get; }
@@ -256,7 +256,7 @@ public unsafe ref struct SeStringDrawState
/// Offset of the glyph in pixels w.r.t. .
internal void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset)
{
- var texId = this.Font->ContainerAtlas->Textures.Ref(g.TextureIndex).TexID;
+ var texId = this.Font.ContainerAtlas.Textures.Ref(g.TextureIndex).TexID;
var xy0 = new Vector2(
MathF.Round(g.X0 * this.FontSizeScale),
MathF.Round(g.Y0 * this.FontSizeScale));
@@ -313,7 +313,7 @@ public unsafe ref struct SeStringDrawState
offset += this.ScreenOffset;
offset.Y += (this.LinkUnderlineThickness - 1) / 2f;
- offset.Y += MathF.Round(((this.LineHeight - this.FontSize) / 2) + (this.Font->Ascent * this.FontSizeScale));
+ offset.Y += MathF.Round(((this.LineHeight - this.FontSize) / 2) + (this.Font.Ascent * this.FontSizeScale));
this.SetCurrentChannel(SeStringDrawChannel.Foreground);
this.DrawList.AddLine(
@@ -340,9 +340,9 @@ public unsafe ref struct SeStringDrawState
internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune)
{
var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue
- ? this.Font->FindGlyph((ushort)rune.Value)
- : this.Font->FallbackGlyph;
- return ref *(ImGuiHelpers.ImFontGlyphReal*)p;
+ ? (ImFontGlyphPtr)this.Font.FindGlyph((ushort)rune.Value)
+ : this.Font.FallbackGlyph;
+ return ref *(ImGuiHelpers.ImFontGlyphReal*)p.Handle;
}
/// Gets the glyph corresponding to the given codepoint.
@@ -375,7 +375,7 @@ public unsafe ref struct SeStringDrawState
return 0;
return MathF.Round(
- this.Font->GetDistanceAdjustmentForPair(
+ this.Font.GetDistanceAdjustmentForPair(
(ushort)left.Value,
(ushort)right.Value) * this.FontSizeScale);
}
From e032840ac8e7ac13cd0493a30cac4398bcd86d94 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 6 Dec 2025 18:32:03 +0100
Subject: [PATCH 104/201] Clean up crash handler window log for external events
---
Dalamud.Boot/crashhandler_shared.h | 2 ++
Dalamud.Boot/veh.cpp | 2 --
DalamudCrashHandler/DalamudCrashHandler.cpp | 26 ++++++++++++++++++---
3 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/Dalamud.Boot/crashhandler_shared.h b/Dalamud.Boot/crashhandler_shared.h
index 8d93e4460..0308306ce 100644
--- a/Dalamud.Boot/crashhandler_shared.h
+++ b/Dalamud.Boot/crashhandler_shared.h
@@ -6,6 +6,8 @@
#define WIN32_LEAN_AND_MEAN
#include
+#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
+
struct exception_info
{
LPEXCEPTION_POINTERS pExceptionPointers;
diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp
index 25c9b5045..50ac9b34c 100644
--- a/Dalamud.Boot/veh.cpp
+++ b/Dalamud.Boot/veh.cpp
@@ -11,8 +11,6 @@
#include "crashhandler_shared.h"
#include "DalamudStartInfo.h"
-#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
-
#pragma comment(lib, "comctl32.lib")
#if defined _M_IX86
diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp
index 107261541..1feec4b2f 100644
--- a/DalamudCrashHandler/DalamudCrashHandler.cpp
+++ b/DalamudCrashHandler/DalamudCrashHandler.cpp
@@ -938,9 +938,19 @@ int main() {
} while (false);
}
+ const bool is_external_event = exinfo.ExceptionRecord.ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT;
+
std::wostringstream log;
- log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl;
- log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl;
+
+ if (!is_external_event)
+ {
+ log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl;
+ log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl;
+ }
+ else
+ {
+ log << L"CLR error occurred" << std::endl;
+ }
if (shutup)
log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl;
@@ -957,9 +967,19 @@ int main() {
if (pProgressDialog)
pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL);
+ std::wstring window_log_str;
+
+ // Cut the log here for external events, the rest is unreadable and doesn't matter since we can't get
+ // symbols for mixed-mode stacks yet.
+ if (is_external_event)
+ window_log_str = log.str();
+
SymRefreshModuleList(GetCurrentProcess());
print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log);
- const auto window_log_str = log.str();
+
+ if (!is_external_event)
+ window_log_str = log.str();
+
print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log);
std::wofstream(logPath) << log.str();
From b2d9480f9f83cb64e476d77f25068817beee6790 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 6 Dec 2025 18:38:13 +0100
Subject: [PATCH 105/201] Submit nuke schema
---
.nuke/build.schema.json | 162 ++++++++++++++++++++--------------------
1 file changed, 81 insertions(+), 81 deletions(-)
diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
index 03211ce8f..6ffb3bb01 100644
--- a/.nuke/build.schema.json
+++ b/.nuke/build.schema.json
@@ -1,19 +1,57 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
- "title": "Build Schema",
- "$ref": "#/definitions/build",
"definitions": {
- "build": {
- "type": "object",
+ "Host": {
+ "type": "string",
+ "enum": [
+ "AppVeyor",
+ "AzurePipelines",
+ "Bamboo",
+ "Bitbucket",
+ "Bitrise",
+ "GitHubActions",
+ "GitLab",
+ "Jenkins",
+ "Rider",
+ "SpaceAutomation",
+ "TeamCity",
+ "Terminal",
+ "TravisCI",
+ "VisualStudio",
+ "VSCode"
+ ]
+ },
+ "ExecutableTarget": {
+ "type": "string",
+ "enum": [
+ "CI",
+ "Clean",
+ "Compile",
+ "CompileCImGui",
+ "CompileCImGuizmo",
+ "CompileCImPlot",
+ "CompileDalamud",
+ "CompileDalamudBoot",
+ "CompileDalamudCrashHandler",
+ "CompileImGuiNatives",
+ "CompileInjector",
+ "Restore",
+ "SetCILogging",
+ "Test"
+ ]
+ },
+ "Verbosity": {
+ "type": "string",
+ "description": "",
+ "enum": [
+ "Verbose",
+ "Normal",
+ "Minimal",
+ "Quiet"
+ ]
+ },
+ "NukeBuild": {
"properties": {
- "Configuration": {
- "type": "string",
- "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
- "enum": [
- "Debug",
- "Release"
- ]
- },
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
@@ -23,29 +61,8 @@
"description": "Shows the help text for this build assembly"
},
"Host": {
- "type": "string",
"description": "Host for execution. Default is 'automatic'",
- "enum": [
- "AppVeyor",
- "AzurePipelines",
- "Bamboo",
- "Bitbucket",
- "Bitrise",
- "GitHubActions",
- "GitLab",
- "Jenkins",
- "Rider",
- "SpaceAutomation",
- "TeamCity",
- "Terminal",
- "TravisCI",
- "VisualStudio",
- "VSCode"
- ]
- },
- "IsDocsBuild": {
- "type": "boolean",
- "description": "Whether we are building for documentation - emits generated files"
+ "$ref": "#/definitions/Host"
},
"NoLogo": {
"type": "boolean",
@@ -74,63 +91,46 @@
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
- "type": "string",
- "enum": [
- "CI",
- "Clean",
- "Compile",
- "CompileCImGui",
- "CompileCImGuizmo",
- "CompileCImPlot",
- "CompileDalamud",
- "CompileDalamudBoot",
- "CompileDalamudCrashHandler",
- "CompileImGuiNatives",
- "CompileInjector",
- "Restore",
- "SetCILogging",
- "Test"
- ]
+ "$ref": "#/definitions/ExecutableTarget"
}
},
- "Solution": {
- "type": "string",
- "description": "Path to a solution file that is automatically loaded"
- },
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
- "type": "string",
- "enum": [
- "CI",
- "Clean",
- "Compile",
- "CompileCImGui",
- "CompileCImGuizmo",
- "CompileCImPlot",
- "CompileDalamud",
- "CompileDalamudBoot",
- "CompileDalamudCrashHandler",
- "CompileImGuiNatives",
- "CompileInjector",
- "Restore",
- "SetCILogging",
- "Test"
- ]
+ "$ref": "#/definitions/ExecutableTarget"
}
},
"Verbosity": {
- "type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
- "enum": [
- "Minimal",
- "Normal",
- "Quiet",
- "Verbose"
- ]
+ "$ref": "#/definitions/Verbosity"
}
}
}
- }
-}
\ No newline at end of file
+ },
+ "allOf": [
+ {
+ "properties": {
+ "Configuration": {
+ "type": "string",
+ "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
+ "enum": [
+ "Debug",
+ "Release"
+ ]
+ },
+ "IsDocsBuild": {
+ "type": "boolean",
+ "description": "Whether we are building for documentation - emits generated files"
+ },
+ "Solution": {
+ "type": "string",
+ "description": "Path to a solution file that is automatically loaded"
+ }
+ }
+ },
+ {
+ "$ref": "#/definitions/NukeBuild"
+ }
+ ]
+}
From 3d29157391da8bc50a60c2aed640510647f257ba Mon Sep 17 00:00:00 2001
From: goaaats