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 01/42] 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 02/42] 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 03/42] 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 04/42] 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 4e87b4b0076460195f3be5e2fe76630e41ebec2e Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 12 Nov 2025 20:15:12 +0100
Subject: [PATCH 05/42] 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 06/42] 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 07/42] 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 08/42] 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 09/42] 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 10/42] 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 11/42] 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 12/42] 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 13/42] 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 14/42] 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 15/42] 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 16/42] 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 17/42] 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 18/42] 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 19/42] 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 20/42] 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 21/42] 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 22/42] 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 23/42] 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 24/42] 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 25/42] 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 26/42] 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 27/42] 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 28/42] 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 29/42] 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 30/42] 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 31/42] 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 32/42] 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 33/42] 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 2cef75bbbef1862f85eb582c0b732c58c6b4a135 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Wed, 26 Nov 2025 11:56:30 -0800
Subject: [PATCH 34/42] 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 c661faea6be8142a9005431ddaccf88ac2ce9025 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Wed, 12 Nov 2025 21:49:28 +0100
Subject: [PATCH 35/42] 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 ead1c705a427ee596ca5a4eff741b0d68fe07fc0 Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Sat, 29 Nov 2025 17:07:51 -0800
Subject: [PATCH 36/42] 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 874745651b1be57b391a078c627a66d7932e30aa Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Sat, 29 Nov 2025 21:07:51 -0800
Subject: [PATCH 37/42] 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 0e6dae9f6476050eaca0e01d2560bbbb136b123d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 3 Dec 2025 18:39:04 +0000
Subject: [PATCH 38/42] 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 39/42] 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 40/42] 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 41/42] 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 42/42] 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));
- }
}