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/43] 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/43] 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/43] 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/43] 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 2a60bc61a7713172bf60ff1b2b78076b44473ac4 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Thu, 27 Nov 2025 15:52:18 -0800
Subject: [PATCH 05/43] Force style vars so erroring window renders at least
partially sanely
---
Dalamud/Interface/Windowing/Window.cs | 47 ++++++++++++++++++++++++---
1 file changed, 42 insertions(+), 5 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index f12e87099..5169b9746 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -57,6 +57,7 @@ public abstract class Window
private bool hasError = false;
private Exception? lastError;
+ private bool isErrorStylePushed;
///
/// Initializes a new instance of the class.
@@ -425,8 +426,16 @@ public abstract class Window
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
}
- this.PreDraw();
- this.ApplyConditionals();
+ if (!this.hasError)
+ {
+ this.PreDraw();
+ this.ApplyConditionals();
+ }
+ else
+ {
+ Style.StyleModelV1.DalamudStandard.Push();
+ this.isErrorStylePushed = true;
+ }
if (this.ForceMainWindow)
ImGuiHelpers.ForceNextWindowMainViewport();
@@ -448,10 +457,28 @@ public abstract class Window
var flags = this.Flags;
if (this.internalIsPinned || this.internalIsClickthrough)
- flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
+ {
+ if (!this.hasError)
+ {
+ flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
+ }
+ else
+ {
+ flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize);
+ }
+ }
if (this.internalIsClickthrough)
- flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
+ {
+ if (!this.hasError)
+ {
+ flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
+ }
+ else
+ {
+ flags &= ~(ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs);
+ }
+ }
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
{
@@ -670,7 +697,17 @@ public abstract class Window
Task.FromResult(tex));
}
- this.PostDraw();
+ if (!this.hasError)
+ {
+ this.PostDraw();
+ }
+ else
+ {
+ if (this.isErrorStylePushed)
+ {
+ Style.StyleModelV1.DalamudStandard.Pop();
+ }
+ }
this.PostHandlePreset(persistence);
From 2e246967317427d2d5749d69b24a3e8ebc77d328 Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Sun, 30 Nov 2025 14:47:24 -0800
Subject: [PATCH 06/43] Set flags, and unlock size
---
Dalamud/Interface/Windowing/Window.cs | 26 ++++++++++----------------
1 file changed, 10 insertions(+), 16 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index 5169b9746..700481ce5 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -458,26 +458,20 @@ public abstract class Window
if (this.internalIsPinned || this.internalIsClickthrough)
{
- if (!this.hasError)
- {
- flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
- }
- else
- {
- flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize);
- }
+ flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
}
if (this.internalIsClickthrough)
{
- if (!this.hasError)
- {
- flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
- }
- else
- {
- flags &= ~(ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs);
- }
+ flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs;
+ }
+
+ // If we have an error, reset all flags to default, and unlock window size.
+ if (this.hasError)
+ {
+ flags = ImGuiWindowFlags.None;
+ ImGui.SetNextWindowCollapsed(false, ImGuiCond.Once);
+ ImGui.SetNextWindowSizeConstraints(Vector2.Zero, Vector2.PositiveInfinity);
}
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
From fb229a0a128dd36b3e531be7bd00d260649ce379 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Mon, 1 Dec 2025 12:07:34 +0100
Subject: [PATCH 07/43] Fix PlayerState.Level being synced
---
Dalamud/Game/Player/PlayerState.cs | 2 +-
Dalamud/Plugin/Services/IPlayerState.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Game/Player/PlayerState.cs b/Dalamud/Game/Player/PlayerState.cs
index 917c946db..bd19b5bfb 100644
--- a/Dalamud/Game/Player/PlayerState.cs
+++ b/Dalamud/Game/Player/PlayerState.cs
@@ -77,7 +77,7 @@ internal unsafe class PlayerState : IServiceType, IPlayerState
public RowRef ClassJob => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->CurrentClassJobId) : default;
///
- public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default;
+ public short Level => this.IsLoaded && this.ClassJob.IsValid ? this.GetClassJobLevel(this.ClassJob.Value) : this.EffectiveLevel;
///
public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced;
diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs
index 1416dfb77..21d88010b 100644
--- a/Dalamud/Plugin/Services/IPlayerState.cs
+++ b/Dalamud/Plugin/Services/IPlayerState.cs
@@ -79,7 +79,7 @@ public interface IPlayerState : IDalamudService
bool IsLevelSynced { get; }
///
- /// Gets the effective level of the local character.
+ /// Gets the effective level of the local character, taking level sync into account.
///
short EffectiveLevel { get; }
From 14e97a1a374b7968f28dc856203e9ba990c31ccd Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Mon, 1 Dec 2025 14:19:12 -0800
Subject: [PATCH 08/43] Use local variable to track pushed style state
---
Dalamud/Interface/Windowing/Window.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index 700481ce5..e90e38119 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -57,7 +57,6 @@ public abstract class Window
private bool hasError = false;
private Exception? lastError;
- private bool isErrorStylePushed;
///
/// Initializes a new instance of the class.
@@ -426,6 +425,7 @@ public abstract class Window
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
}
+ var isErrorStylePushed = false;
if (!this.hasError)
{
this.PreDraw();
@@ -434,7 +434,7 @@ public abstract class Window
else
{
Style.StyleModelV1.DalamudStandard.Push();
- this.isErrorStylePushed = true;
+ isErrorStylePushed = true;
}
if (this.ForceMainWindow)
@@ -697,7 +697,7 @@ public abstract class Window
}
else
{
- if (this.isErrorStylePushed)
+ if (isErrorStylePushed)
{
Style.StyleModelV1.DalamudStandard.Pop();
}
From 518b3a4fb351eb1a826d3712f1c7fc4b712d658f Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Wed, 3 Dec 2025 16:43:12 +0100
Subject: [PATCH 09/43] Fix NounProcessor BeastTribe column offset
---
Dalamud/Game/Text/Noun/NounParams.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Game/Text/Noun/NounParams.cs b/Dalamud/Game/Text/Noun/NounParams.cs
index 3d5c424be..ab7a732d2 100644
--- a/Dalamud/Game/Text/Noun/NounParams.cs
+++ b/Dalamud/Game/Text/Noun/NounParams.cs
@@ -60,8 +60,8 @@ internal record struct NounParams()
///
public readonly int ColumnOffset => this.SheetName switch
{
- // See "E8 ?? ?? ?? ?? 44 8B 6B 08"
- nameof(LSheets.BeastTribe) => 10,
+ // See "E8 ?? ?? ?? ?? 44 8B 66 ?? 8B E8"
+ nameof(LSheets.BeastTribe) => 11,
nameof(LSheets.DeepDungeonItem) => 1,
nameof(LSheets.DeepDungeonEquipment) => 1,
nameof(LSheets.DeepDungeonMagicStone) => 1,
From f198ce46dc1d2100d95d4c090e446077e2af5729 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Wed, 3 Dec 2025 16:47:13 +0100
Subject: [PATCH 10/43] Add self tests for ColumnOffset
---
.../Steps/NounProcessorSelfTestStep.cs | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs
index ccb23d395..ccccc691c 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs
@@ -191,6 +191,29 @@ internal class NounProcessorSelfTestStep : ISelfTestStep
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"),
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"),
new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"),
+
+ // ColumnOffset tests
+
+ new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a Amalj'aa"),
+ new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Amalj'aa"),
+
+ new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an aetherpool arm"),
+ new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the aetherpool arm"),
+
+ new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pomander of safety"),
+ new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pomander of safety"),
+
+ new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a splinter of Inferno magicite"),
+ new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the splinter of Inferno magicite"),
+
+ new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an Unei demiclone"),
+ new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Unei demiclone"),
+
+ new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pair of oval spectacles"),
+ new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pair of oval spectacles"),
+
+ new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a shaded spectacles"),
+ new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the shaded spectacles"),
];
private enum GermanCases
From 0e6dae9f6476050eaca0e01d2560bbbb136b123d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 3 Dec 2025 18:39:04 +0000
Subject: [PATCH 11/43] 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 12/43] 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 13/43] 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 14/43] 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 15/43] Access custom font atlas fields directly through
bindings
---
.../Utility/BufferBackedImDrawData.cs | 24 ++-----------------
1 file changed, 2 insertions(+), 22 deletions(-)
diff --git a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
index 112fda8a8..e6128992a 100644
--- a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
+++ b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs
@@ -39,9 +39,8 @@ public unsafe struct BufferBackedImDrawData : IDisposable
*ds = default;
var atlas = ImGui.GetIO().Fonts;
- ref var atlasTail = ref ImFontAtlasTailReal.From(atlas);
ds->SharedData = *ImGui.GetDrawListSharedData().Handle;
- ds->SharedData.TexIdCommon = atlas.Textures[atlasTail.TextureIndexCommon].TexID;
+ ds->SharedData.TexIdCommon = atlas.Textures[atlas.TextureIndexCommon].TexID;
ds->SharedData.TexUvWhitePixel = atlas.TexUvWhitePixel;
ds->SharedData.TexUvLines = (Vector4*)Unsafe.AsPointer(ref atlas.TexUvLines[0]);
ds->SharedData.Font = ImGui.GetIO().FontDefault;
@@ -60,7 +59,7 @@ public unsafe struct BufferBackedImDrawData : IDisposable
res.ListPtr._ResetForNewFrame();
res.ListPtr.PushClipRectFullScreen();
- res.ListPtr.PushTextureID(new(atlasTail.TextureIndexCommon));
+ res.ListPtr.PushTextureID(new(atlas.TextureIndexCommon));
return res;
}
@@ -90,23 +89,4 @@ public unsafe struct BufferBackedImDrawData : IDisposable
public ImDrawList List;
public ImDrawListSharedData SharedData;
}
-
- [StructLayout(LayoutKind.Sequential)]
- private struct ImFontAtlasTailReal
- {
- /// Index of texture containing the below.
- public int TextureIndexCommon;
-
- /// Custom texture rectangle ID for both of the below.
- public int PackIdCommon;
-
- /// Custom texture rectangle for white pixel and mouse cursors.
- public ImFontAtlasCustomRect RectMouseCursors;
-
- /// Custom texture rectangle for baked anti-aliased lines.
- public ImFontAtlasCustomRect RectLines;
-
- public static ref ImFontAtlasTailReal From(ImFontAtlasPtr fontAtlasPtr) =>
- ref *(ImFontAtlasTailReal*)(&fontAtlasPtr.Handle->FontBuilderFlags + sizeof(uint));
- }
}
From ddc743aae1337f968223ce791a3720d48b0e71b8 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Thu, 4 Dec 2025 23:00:36 +0100
Subject: [PATCH 16/43] Note that font ptr must be supplied when setting
TargetDrawList
---
.../Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
index f3d4c44e9..1d8126f3b 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
@@ -12,7 +12,10 @@ public record struct SeStringDrawParams
/// Gets or sets the target draw list.
/// Target draw list, default(ImDrawListPtr) to not draw, or null to use
/// (the default).
- /// If this value is set, will not be called, and ImGui ID will be ignored.
+ ///
+ /// If this value is set, will not be called, and ImGui ID will be ignored.
+ /// You must specify a valid draw list and a valid font via if you set this value,
+ /// since the renderer will not be able to retrieve them from ImGui context.
///
public ImDrawListPtr? TargetDrawList { get; set; }
From 3c7dbf9f81e147e45f7b3541844e95cebcc3a5df Mon Sep 17 00:00:00 2001
From: MidoriKami
Date: Fri, 5 Dec 2025 16:59:17 -0800
Subject: [PATCH 17/43] Remove AddonEventManagerAddressResolver.cs
---
.../Game/Addon/Events/AddonEventManager.cs | 30 ++++++++-----------
.../AddonEventManagerAddressResolver.cs | 21 -------------
2 files changed, 12 insertions(+), 39 deletions(-)
delete mode 100644 Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs
index 945197e2b..980404940 100644
--- a/Dalamud/Game/Addon/Events/AddonEventManager.cs
+++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs
@@ -9,7 +9,6 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events;
@@ -32,25 +31,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService
private readonly AddonLifecycleEventListener finalizeEventListener;
- private readonly AddonEventManagerAddressResolver address;
- private readonly Hook onUpdateCursor;
+ private readonly Hook onUpdateCursor;
private readonly ConcurrentDictionary pluginEventControllers;
- private AddonCursorType? cursorOverride;
+ private AtkCursor.CursorType? cursorOverride;
[ServiceManager.ServiceConstructor]
- private AddonEventManager(TargetSigScanner sigScanner)
+ private AddonEventManager()
{
- this.address = new AddonEventManagerAddressResolver();
- this.address.Setup(sigScanner);
-
this.pluginEventControllers = new ConcurrentDictionary();
this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
this.cursorOverride = null;
- this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
+ this.onUpdateCursor = Hook.FromAddress(AtkUnitManager.Addresses.UpdateCursor.Value, this.UpdateCursorDetour);
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
this.addonLifecycle.RegisterListener(this.finalizeEventListener);
@@ -58,8 +53,6 @@ internal unsafe class AddonEventManager : IInternalDisposableService
this.onUpdateCursor.Enable();
}
- private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
-
///
void IInternalDisposableService.DisposeService()
{
@@ -117,7 +110,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
/// Force the game cursor to be the specified cursor.
///
/// Which cursor to use.
- internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
+ internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = (AtkCursor.CursorType)cursor;
///
/// Un-forces the game cursor.
@@ -168,7 +161,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
}
}
- private nint UpdateCursorDetour(RaptureAtkModule* module)
+ private void UpdateCursorDetour(AtkUnitManager* thisPtr)
{
try
{
@@ -176,13 +169,14 @@ internal unsafe class AddonEventManager : IInternalDisposableService
if (this.cursorOverride is not null && atkStage is not null)
{
- var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
- if (cursor != this.cursorOverride)
+ ref var atkCursor = ref atkStage->AtkCursor;
+
+ if (atkCursor.Type != this.cursorOverride)
{
- AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
+ atkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
}
- return nint.Zero;
+ return;
}
}
catch (Exception e)
@@ -190,7 +184,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
Log.Error(e, "Exception in UpdateCursorDetour.");
}
- return this.onUpdateCursor!.Original(module);
+ this.onUpdateCursor!.Original(thisPtr);
}
}
diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
deleted file mode 100644
index 415e1b169..000000000
--- a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Dalamud.Game.Addon.Events;
-
-///
-/// AddonEventManager memory address resolver.
-///
-internal class AddonEventManagerAddressResolver : BaseAddressResolver
-{
- ///
- /// Gets the address of the AtkModule UpdateCursor method.
- ///
- public nint UpdateCursor { get; private set; }
-
- ///
- /// Scan for and setup any configured address pointers.
- ///
- /// The signature scanner to facilitate setup.
- protected override void Setup64Bit(ISigScanner scanner)
- {
- this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS
- }
-}
From 2e5c560ed729b7e88e6f036aa9987bca5e6f7be0 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Sat, 6 Dec 2025 12:48:34 +0000
Subject: [PATCH 18/43] Update ClientStructs
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index e5dedba42..6f339d8f7 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f
+Subproject commit 6f339d8f725fa6922449f7e5c584ca6b8fa2fb19
From 9c2d2b7c1dbe192599748e7320fce3c1cc972072 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 6 Dec 2025 15:07:09 +0100
Subject: [PATCH 19/43] Report CLR errors through DalamudCrashHandler/VEH by
hooking ReportEventW
---
Dalamud.Boot/dllmain.cpp | 41 ++++++++++++++++++++++++++++++++++++++++
Dalamud.Boot/veh.cpp | 37 ++++++++++++++++++++++++++++--------
Dalamud.Boot/veh.h | 1 +
3 files changed, 71 insertions(+), 8 deletions(-)
diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp
index 80a16f89a..2af78092d 100644
--- a/Dalamud.Boot/dllmain.cpp
+++ b/Dalamud.Boot/dllmain.cpp
@@ -331,6 +331,47 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("VEH was disabled manually");
}
+ // ============================== CLR Reporting =================================== //
+
+ // This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it
+ // was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now.
+ // Ideally all of this will go away once they get to it.
+ static std::shared_ptr> s_hook;
+ s_hook = std::make_shared>("advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
+ s_hook->set_detour([hook = s_hook.get()]( HANDLE hEventLog,
+ WORD wType,
+ WORD wCategory,
+ DWORD dwEventID,
+ PSID lpUserSid,
+ WORD wNumStrings,
+ DWORD dwDataSize,
+ LPCWSTR* lpStrings,
+ LPVOID lpRawData)->BOOL {
+
+ // Check for CLR Error Event IDs
+ if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception
+ dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast
+ dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime
+ dwEventID != 1027 && // ERT_StackOverflow: The process was terminated due to a stack overflow
+ dwEventID != 1028) // ERT_CodeContractFailed: The application encountered a bug. A managed code contract (precondition, postcondition, object invariant, or assert) failed
+ {
+ return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
+ }
+
+ if (wNumStrings == 0 || lpStrings == nullptr) {
+ logging::W("ReportEventW called with no strings.");
+ return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
+ }
+
+ // In most cases, DalamudCrashHandler will kill us now, so call original here to make sure we still write to the event log.
+ const BOOL original_ret = hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
+
+ const std::wstring error_details(lpStrings[0]);
+ veh::raise_external_event(error_details);
+
+ return original_ret;
+ });
+
// ============================== Dalamud ==================================== //
if (static_cast(g_startInfo.BootWaitMessageBox) & static_cast(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))
diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp
index b0ec1cefa..24a846e66 100644
--- a/Dalamud.Boot/veh.cpp
+++ b/Dalamud.Boot/veh.cpp
@@ -11,6 +11,8 @@
#include "crashhandler_shared.h"
#include "DalamudStartInfo.h"
+#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
+
#pragma comment(lib, "comctl32.lib")
#if defined _M_IX86
@@ -31,6 +33,8 @@ HANDLE g_crashhandler_process = nullptr;
HANDLE g_crashhandler_event = nullptr;
HANDLE g_crashhandler_pipe_write = nullptr;
+wchar_t g_external_event_info[16384] = L"";
+
std::recursive_mutex g_exception_handler_mutex;
std::chrono::time_point g_time_start;
@@ -190,7 +194,11 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
std::wstring stackTrace;
- if (!g_clr)
+ if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
+ {
+ stackTrace = std::wstring(g_external_event_info);
+ }
+ else if (!g_clr)
{
stackTrace = L"(no CLR stack trace available)";
}
@@ -251,6 +259,12 @@ LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
{
+ // special case for CLR exceptions, always trigger crash handler
+ if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
+ {
+ return exception_handler(ex);
+ }
+
if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{
// pass
@@ -268,7 +282,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) &&
!is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip))
- return EXCEPTION_CONTINUE_SEARCH;
+ return EXCEPTION_CONTINUE_SEARCH;
}
return exception_handler(ex);
@@ -297,7 +311,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536))
{
hWritePipe.emplace(hWritePipeRaw, &CloseHandle);
-
+
if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE))
{
hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle);
@@ -315,9 +329,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
}
// additional information
- STARTUPINFOEXW siex{};
+ STARTUPINFOEXW siex{};
PROCESS_INFORMATION pi{};
-
+
siex.StartupInfo.cb = sizeof siex;
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE;
@@ -385,7 +399,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
argstr.push_back(L' ');
}
argstr.pop_back();
-
+
if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr))
{
logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError());
@@ -400,7 +414,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
TRUE, // Set handle inheritance to FALSE
EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W)
nullptr, // Use parent's environment block
- nullptr, // Use parent's starting directory
+ nullptr, // Use parent's starting directory
&siex.StartupInfo, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
))
@@ -416,7 +430,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
}
CloseHandle(pi.hThread);
-
+
g_crashhandler_process = pi.hProcess;
g_crashhandler_pipe_write = hWritePipe->release();
logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId);
@@ -434,3 +448,10 @@ bool veh::remove_handler()
}
return false;
}
+
+void veh::raise_external_event(const std::wstring& errorMessage)
+{
+ const auto info_size = std::min(errorMessage.size(), std::size(g_external_event_info) - 1);
+ wcsncpy_s(g_external_event_info, errorMessage.c_str(), info_size);
+ RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr);
+}
diff --git a/Dalamud.Boot/veh.h b/Dalamud.Boot/veh.h
index 1905272ea..2a02c374e 100644
--- a/Dalamud.Boot/veh.h
+++ b/Dalamud.Boot/veh.h
@@ -4,4 +4,5 @@ namespace veh
{
bool add_handler(bool doFullDump, const std::string& workingDirectory);
bool remove_handler();
+ void raise_external_event(const std::wstring& info);
}
From e09c43b8decc55fcbc38cd61102256db0bdc4e29 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 6 Dec 2025 15:07:46 +0100
Subject: [PATCH 20/43] Fix bad exit condition when looping exception records
---
DalamudCrashHandler/DalamudCrashHandler.cpp | 78 ++++++++++++---------
1 file changed, 43 insertions(+), 35 deletions(-)
diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp
index ec7115ffd..107261541 100644
--- a/DalamudCrashHandler/DalamudCrashHandler.cpp
+++ b/DalamudCrashHandler/DalamudCrashHandler.cpp
@@ -119,7 +119,7 @@ std::wstring describe_module(const std::filesystem::path& path) {
return std::format(L"", GetLastError());
UINT size = 0;
-
+
std::wstring version = L"v?.?.?.?";
if (LPVOID lpBuffer; VerQueryValueW(block.data(), L"\\", &lpBuffer, &size)) {
const auto& v = *static_cast(lpBuffer);
@@ -176,7 +176,7 @@ const std::map& get_remote_modules() {
std::vector buf(8192);
for (size_t i = 0; i < 64; i++) {
if (DWORD needed; !EnumProcessModules(g_hProcess, &buf[0], static_cast(std::span(buf).size_bytes()), &needed)) {
- std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl;
+ std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl;
break;
} else if (needed > std::span(buf).size_bytes()) {
buf.resize(needed / sizeof(HMODULE) + 16);
@@ -201,7 +201,7 @@ const std::map& get_remote_modules() {
data[hModule] = nth64.OptionalHeader.SizeOfImage;
}
-
+
return data;
}();
@@ -292,35 +292,43 @@ std::wstring to_address_string(const DWORD64 address, const bool try_ptrderef =
void print_exception_info(HANDLE hThread, const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log) {
std::vector exRecs;
- if (ex.ExceptionRecord) {
+ if (ex.ExceptionRecord)
+ {
size_t rec_index = 0;
size_t read;
- exRecs.emplace_back();
+
for (auto pRemoteExRec = ex.ExceptionRecord;
- pRemoteExRec
- && rec_index < 64
- && ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read)
- && read >= offsetof(EXCEPTION_RECORD, ExceptionInformation)
- && read >= static_cast(reinterpret_cast(&exRecs.back().ExceptionInformation[exRecs.back().NumberParameters]) - reinterpret_cast(&exRecs.back()));
- rec_index++) {
+ pRemoteExRec && rec_index < 64;
+ rec_index++)
+ {
+ exRecs.emplace_back();
+
+ if (!ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read)
+ || read < offsetof(EXCEPTION_RECORD, ExceptionInformation)
+ || read < static_cast(reinterpret_cast(&exRecs.back().ExceptionInformation[exRecs.
+ back().NumberParameters]) - reinterpret_cast(&exRecs.back())))
+ {
+ exRecs.pop_back();
+ break;
+ }
log << std::format(L"\nException Info #{}\n", rec_index);
log << std::format(L"Address: {:X}\n", exRecs.back().ExceptionCode);
log << std::format(L"Flags: {:X}\n", exRecs.back().ExceptionFlags);
log << std::format(L"Address: {:X}\n", reinterpret_cast(exRecs.back().ExceptionAddress));
- if (!exRecs.back().NumberParameters)
- continue;
- log << L"Parameters: ";
- for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i) {
- if (i != 0)
- log << L", ";
- log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]);
+ if (exRecs.back().NumberParameters)
+ {
+ log << L"Parameters: ";
+ for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i)
+ {
+ if (i != 0)
+ log << L", ";
+ log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]);
+ }
}
pRemoteExRec = exRecs.back().ExceptionRecord;
- exRecs.emplace_back();
}
- exRecs.pop_back();
}
log << L"\nCall Stack\n{";
@@ -410,7 +418,7 @@ void print_exception_info_extended(const EXCEPTION_POINTERS& ex, const CONTEXT&
std::wstring escape_shell_arg(const std::wstring& arg) {
// https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
-
+
std::wstring res;
if (!arg.empty() && arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) {
res.append(arg);
@@ -504,7 +512,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
filePath.emplace(pFilePath);
std::fstream fileStream(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);
-
+
mz_zip_archive zipa{};
zipa.m_pIO_opaque = &fileStream;
zipa.m_pRead = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t {
@@ -566,7 +574,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s
const auto hLogFile = CreateFileW(logFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hLogFile == INVALID_HANDLE_VALUE)
throw_last_error(std::format("indiv. log file: CreateFileW({})", ws_to_u8(logFilePath.wstring())));
-
+
std::unique_ptr hLogFileClose(hLogFile, &CloseHandle);
LARGE_INTEGER size, baseOffset{};
@@ -695,7 +703,7 @@ int main() {
// IFileSaveDialog only works on STA
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
-
+
std::vector args;
if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) {
for (auto i = 0; i < argc; i++)
@@ -823,14 +831,14 @@ int main() {
hr = pOleWindow->GetWindow(&hwndProgressDialog);
if (SUCCEEDED(hr))
{
- SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0,
+ SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
SetForegroundWindow(hwndProgressDialog);
}
-
+
pOleWindow->Release();
}
-
+
}
else {
std::cerr << "Failed to create progress window" << std::endl;
@@ -852,14 +860,14 @@ int main() {
https://github.com/sumatrapdfreader/sumatrapdf/blob/master/src/utils/DbgHelpDyn.cpp
*/
-
+
if (g_bSymbolsAvailable) {
SymRefreshModuleList(g_hProcess);
}
else if(!assetDir.empty())
{
auto symbol_search_path = std::format(L".;{}", (assetDir / "UIRes" / "pdb").wstring());
-
+
g_bSymbolsAvailable = SymInitializeW(g_hProcess, symbol_search_path.c_str(), true);
std::wcout << std::format(L"Init symbols with PDB at {}", symbol_search_path) << std::endl;
@@ -870,12 +878,12 @@ int main() {
g_bSymbolsAvailable = SymInitializeW(g_hProcess, nullptr, true);
std::cout << "Init symbols without PDB" << std::endl;
}
-
+
if (!g_bSymbolsAvailable) {
std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl;
}
- if (pProgressDialog)
+ if (pProgressDialog)
pProgressDialog->SetLine(3, L"Reading troubleshooting data", FALSE, NULL);
std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0');
@@ -936,7 +944,7 @@ int main() {
if (shutup)
log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl;
-
+
if (dumpPath.empty())
log << L"Dump skipped" << std::endl;
else if (dumpError.empty())
@@ -1003,7 +1011,7 @@ int main() {
R"aa(Help | Open log directory | Open log file)aa"
);
#endif
-
+
// Can't do this, xiv stops pumping messages here
//config.hwndParent = FindWindowA("FFXIVGAME", NULL);
@@ -1056,13 +1064,13 @@ int main() {
return (*reinterpret_cast(dwRefData))(hwnd, uNotification, wParam, lParam);
};
config.lpCallbackData = reinterpret_cast(&callback);
-
+
if (pProgressDialog) {
pProgressDialog->StopProgressDialog();
pProgressDialog->Release();
pProgressDialog = NULL;
}
-
+
const auto kill_game = [&] { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); };
if (shutup) {
From 446c7e38771a943ac7b2dd9d5b48478f95d66250 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 6 Dec 2025 15:14:13 +0100
Subject: [PATCH 21/43] Some logging, cleanup
---
Dalamud.Boot/dllmain.cpp | 26 +++++++++++++++-----------
Dalamud.Boot/veh.cpp | 6 +++---
2 files changed, 18 insertions(+), 14 deletions(-)
diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp
index 2af78092d..687089f82 100644
--- a/Dalamud.Boot/dllmain.cpp
+++ b/Dalamud.Boot/dllmain.cpp
@@ -336,19 +336,22 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
// This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it
// was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now.
// Ideally all of this will go away once they get to it.
- static std::shared_ptr> s_hook;
- s_hook = std::make_shared>("advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
- s_hook->set_detour([hook = s_hook.get()]( HANDLE hEventLog,
- WORD wType,
- WORD wCategory,
- DWORD dwEventID,
- PSID lpUserSid,
- WORD wNumStrings,
- DWORD dwDataSize,
- LPCWSTR* lpStrings,
- LPVOID lpRawData)->BOOL {
+ static std::shared_ptr> s_report_event_hook;
+ s_report_event_hook = std::make_shared>(
+ "advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
+ s_report_event_hook->set_detour([hook = s_report_event_hook.get()](
+ HANDLE hEventLog,
+ WORD wType,
+ WORD wCategory,
+ DWORD dwEventID,
+ PSID lpUserSid,
+ WORD wNumStrings,
+ DWORD dwDataSize,
+ LPCWSTR* lpStrings,
+ LPVOID lpRawData)-> BOOL {
// Check for CLR Error Event IDs
+ // https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/eventreporter.cpp#L370
if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception
dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast
dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime
@@ -371,6 +374,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
return original_ret;
});
+ logging::I("ReportEventW hook installed.");
// ============================== Dalamud ==================================== //
diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp
index 24a846e66..25c9b5045 100644
--- a/Dalamud.Boot/veh.cpp
+++ b/Dalamud.Boot/veh.cpp
@@ -449,9 +449,9 @@ bool veh::remove_handler()
return false;
}
-void veh::raise_external_event(const std::wstring& errorMessage)
+void veh::raise_external_event(const std::wstring& info)
{
- const auto info_size = std::min(errorMessage.size(), std::size(g_external_event_info) - 1);
- wcsncpy_s(g_external_event_info, errorMessage.c_str(), info_size);
+ const auto info_size = std::min(info.size(), std::size(g_external_event_info) - 1);
+ wcsncpy_s(g_external_event_info, info.c_str(), info_size);
RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr);
}
From e032840ac8e7ac13cd0493a30cac4398bcd86d94 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sat, 6 Dec 2025 18:32:03 +0100
Subject: [PATCH 22/43] Clean up crash handler window log for external events
---
Dalamud.Boot/crashhandler_shared.h | 2 ++
Dalamud.Boot/veh.cpp | 2 --
DalamudCrashHandler/DalamudCrashHandler.cpp | 26 ++++++++++++++++++---
3 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/Dalamud.Boot/crashhandler_shared.h b/Dalamud.Boot/crashhandler_shared.h
index 8d93e4460..0308306ce 100644
--- a/Dalamud.Boot/crashhandler_shared.h
+++ b/Dalamud.Boot/crashhandler_shared.h
@@ -6,6 +6,8 @@
#define WIN32_LEAN_AND_MEAN
#include
+#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
+
struct exception_info
{
LPEXCEPTION_POINTERS pExceptionPointers;
diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp
index 25c9b5045..50ac9b34c 100644
--- a/Dalamud.Boot/veh.cpp
+++ b/Dalamud.Boot/veh.cpp
@@ -11,8 +11,6 @@
#include "crashhandler_shared.h"
#include "DalamudStartInfo.h"
-#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
-
#pragma comment(lib, "comctl32.lib")
#if defined _M_IX86
diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp
index 107261541..1feec4b2f 100644
--- a/DalamudCrashHandler/DalamudCrashHandler.cpp
+++ b/DalamudCrashHandler/DalamudCrashHandler.cpp
@@ -938,9 +938,19 @@ int main() {
} while (false);
}
+ const bool is_external_event = exinfo.ExceptionRecord.ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT;
+
std::wostringstream log;
- log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl;
- log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl;
+
+ if (!is_external_event)
+ {
+ log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl;
+ log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl;
+ }
+ else
+ {
+ log << L"CLR error occurred" << std::endl;
+ }
if (shutup)
log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl;
@@ -957,9 +967,19 @@ int main() {
if (pProgressDialog)
pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL);
+ std::wstring window_log_str;
+
+ // Cut the log here for external events, the rest is unreadable and doesn't matter since we can't get
+ // symbols for mixed-mode stacks yet.
+ if (is_external_event)
+ window_log_str = log.str();
+
SymRefreshModuleList(GetCurrentProcess());
print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log);
- const auto window_log_str = log.str();
+
+ if (!is_external_event)
+ window_log_str = log.str();
+
print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log);
std::wofstream(logPath) << log.str();
From ab5ea34e686e7ead9673f0fb8492ca0a00aa5a09 Mon Sep 17 00:00:00 2001
From: goat <16760685+goaaats@users.noreply.github.com>
Date: Sat, 6 Dec 2025 18:46:06 +0100
Subject: [PATCH 23/43] ci: make deploying builds globally blocking, don't
cancel in-progress
---
.github/workflows/main.yml | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index be44afacc..ea1afcac5 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,9 +1,10 @@
name: Build Dalamud
on: [push, pull_request, workflow_dispatch]
+# Globally blocking because of git pushes in deploy step
concurrency:
- group: build_dalamud_${{ github.ref_name }}
- cancel-in-progress: true
+ group: build_dalamud_${{ github.repository_owner }}
+ cancel-in-progress: false
jobs:
build:
From caa869d3ace57609680782b5862bb1d0820e86b5 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 7 Dec 2025 12:54:13 +0100
Subject: [PATCH 24/43] Clarify exception and docs regarding off-thread drawing
with SeStrings, again
---
.../Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs | 5 ++++-
Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
index 1d8126f3b..972013328 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
@@ -14,8 +14,9 @@ public record struct SeStringDrawParams
/// (the default).
///
/// If this value is set, will not be called, and ImGui ID will be ignored.
- /// You must specify a valid draw list and a valid font via if you set this value,
+ /// You must specify a valid draw list, a valid font via and if you set this value,
/// since the renderer will not be able to retrieve them from ImGui context.
+ /// Must be set when drawing off the main thread.
///
public ImDrawListPtr? TargetDrawList { get; set; }
@@ -29,11 +30,13 @@ public record struct SeStringDrawParams
/// Gets or sets the font to use.
/// Font to use, or null to use (the default).
+ /// Must be set when specifying a target draw-list or drawing off the main thread.
public ImFontPtr? Font { get; set; }
/// Gets or sets the font size.
/// Font size in pixels, or 0 to use the current ImGui font size .
///
+ /// Must be set when specifying a target draw-list or drawing off the main thread.
public float? FontSize { get; set; }
/// Gets or sets the line height ratio.
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
index 11c1120b4..04d0501b1 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -65,7 +65,7 @@ public unsafe ref struct SeStringDrawState
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.");
+ $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state.");
this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue;
this.Color = ssdp.Color ?? uint.MaxValue;
this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread.
From b35faf13b53ce25e0f3f92977a75c4cb154a050b Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 7 Dec 2025 13:04:11 +0100
Subject: [PATCH 25/43] Show unhandled exceptions through VEH
---
Dalamud.Boot/veh.cpp | 6 +++++
Dalamud/EntryPoint.cs | 25 +++----------------
.../Interface/Internal/DalamudInterface.cs | 7 ++++++
Dalamud/Utility/ErrorHandling.cs | 21 ++++++++++++++++
DalamudCrashHandler/DalamudCrashHandler.cpp | 4 +--
5 files changed, 40 insertions(+), 23 deletions(-)
create mode 100644 Dalamud/Utility/ErrorHandling.cs
diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp
index 50ac9b34c..194840e52 100644
--- a/Dalamud.Boot/veh.cpp
+++ b/Dalamud.Boot/veh.cpp
@@ -453,3 +453,9 @@ void veh::raise_external_event(const std::wstring& info)
wcsncpy_s(g_external_event_info, info.c_str(), info_size);
RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr);
}
+
+extern "C" __declspec(dllexport) void BootVehRaiseExternalEventW(LPCWSTR info)
+{
+ const std::wstring info_wstr(info);
+ veh::raise_external_event(info_wstr);
+}
diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs
index 15077f3d8..0f8cb0480 100644
--- a/Dalamud/EntryPoint.cs
+++ b/Dalamud/EntryPoint.cs
@@ -292,7 +292,6 @@ public sealed class EntryPoint
}
var pluginInfo = string.Empty;
- var supportText = ", please visit us on Discord for more help";
try
{
var pm = Service.GetNullable();
@@ -300,9 +299,6 @@ public sealed class EntryPoint
if (plugin != null)
{
pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n";
-
- if (plugin.IsThirdParty)
- supportText = string.Empty;
}
}
catch
@@ -310,31 +306,18 @@ public sealed class EntryPoint
// ignored
}
- const MESSAGEBOX_STYLE flags = MESSAGEBOX_STYLE.MB_YESNO | MESSAGEBOX_STYLE.MB_ICONERROR | MESSAGEBOX_STYLE.MB_SYSTEMMODAL;
- var result = Windows.Win32.PInvoke.MessageBox(
- new HWND(Process.GetCurrentProcess().MainWindowHandle),
- $"An internal error in a Dalamud plugin occurred.\nThe game must close.\n\n{ex.GetType().Name}\n{info}\n\n{pluginInfo}More information has been recorded separately{supportText}.\n\nDo you want to disable all plugins the next time you start the game?",
- "Dalamud",
- flags);
-
- if (result == MESSAGEBOX_RESULT.IDYES)
- {
- Log.Information("User chose to disable plugins on next launch...");
- var config = Service.Get();
- config.PluginSafeMode = true;
- config.ForceSave();
- }
-
Log.CloseAndFlush();
- Environment.Exit(-1);
+
+ ErrorHandling.CrashWithContext($"{ex}\n\n{info}\n\n{pluginInfo}");
break;
default:
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
Log.CloseAndFlush();
- Environment.Exit(-1);
break;
}
+
+ Environment.Exit(-1);
}
private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args)
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index af78c5b0c..bf55a5486 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -667,6 +667,8 @@ internal class DalamudInterface : IInternalDisposableService
{
if (this.isImGuiDrawDevMenu)
{
+ using var barColor = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0.060f, 0.060f, 0.060f, 0.773f));
+ barColor.Push(ImGuiCol.MenuBarBg, Vector4.Zero);
if (ImGui.BeginMainMenuBar())
{
var pluginManager = Service.Get();
@@ -839,6 +841,11 @@ internal class DalamudInterface : IInternalDisposableService
ImGui.PopStyleVar();
}
+ if (ImGui.MenuItem("Raise external event through boot"))
+ {
+ ErrorHandling.CrashWithContext("Tést");
+ }
+
ImGui.EndMenu();
}
diff --git a/Dalamud/Utility/ErrorHandling.cs b/Dalamud/Utility/ErrorHandling.cs
new file mode 100644
index 000000000..3c025a12e
--- /dev/null
+++ b/Dalamud/Utility/ErrorHandling.cs
@@ -0,0 +1,21 @@
+using System.Runtime.InteropServices;
+
+namespace Dalamud.Utility;
+
+///
+/// Utilities for handling errors inside Dalamud.
+///
+internal static partial class ErrorHandling
+{
+ ///
+ /// Crash the game at this point, and show the crash handler with the supplied context.
+ ///
+ /// The context to show in the crash handler.
+ public static void CrashWithContext(string context)
+ {
+ BootVehRaiseExternalEvent(context);
+ }
+
+ [LibraryImport("Dalamud.Boot.dll", EntryPoint = "BootVehRaiseExternalEventW", StringMarshalling = StringMarshalling.Utf16)]
+ private static partial void BootVehRaiseExternalEvent(string info);
+}
diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp
index 1feec4b2f..3955bd983 100644
--- a/DalamudCrashHandler/DalamudCrashHandler.cpp
+++ b/DalamudCrashHandler/DalamudCrashHandler.cpp
@@ -1014,8 +1014,8 @@ int main() {
config.pButtons = buttons;
config.cButtons = ARRAYSIZE(buttons);
config.nDefaultButton = IdButtonRestart;
- config.pszExpandedControlText = L"Hide stack trace";
- config.pszCollapsedControlText = L"Stack trace for plugin developers";
+ config.pszExpandedControlText = L"Hide further information";
+ config.pszCollapsedControlText = L"Further information for developers";
config.pszExpandedInformation = window_log_str.c_str();
config.pszWindowTitle = L"Dalamud Crash Handler";
config.pRadioButtons = radios;
From c50237cf6604f32de59c070667abab6813534b84 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 7 Dec 2025 15:46:01 +0100
Subject: [PATCH 26/43] Add compatibility changes for SeString API breakage
---
.../Internal/SeStringRenderer.cs | 4 +++-
.../ImGuiSeStringRenderer/SeStringDrawState.cs | 16 ++++++++++++++--
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
index 87df2da2c..397502b30 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
@@ -161,7 +161,9 @@ internal class SeStringRenderer : IServiceType
ImFont* font = null;
if (drawParams.Font.HasValue)
font = drawParams.Font.Value;
- if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null)
+
+ // API14: Remove commented out code
+ if (ThreadSafety.IsMainThread /* && drawParams.TargetDrawList is null */ && font is null)
font = ImGui.GetFont();
if (font is null)
throw new ArgumentException("Specified font is empty.");
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
index 04d0501b1..5e63ef160 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -64,8 +64,20 @@ public unsafe ref struct SeStringDrawState
{
this.drawList = ssdp.TargetDrawList.Value;
this.ScreenOffset = Vector2.Zero;
- this.FontSize = ssdp.FontSize ?? throw new ArgumentException(
- $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state.");
+
+ // API14: Remove, always throw
+ if (ThreadSafety.IsMainThread)
+ {
+ this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
+ }
+ else
+ {
+ throw new ArgumentException(
+ $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state.");
+ }
+
+ // this.FontSize = ssdp.FontSize ?? throw new ArgumentException(
+ // $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state.");
this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue;
this.Color = ssdp.Color ?? uint.MaxValue;
this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread.
From 094483e5a08315b218f1e20277db44df360cbc2f Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 7 Dec 2025 15:52:13 +0100
Subject: [PATCH 27/43] List PRs in changelog generator
---
.github/generate_changelog.py | 209 +++++++++++++++++------
.github/workflows/generate-changelog.yml | 24 +--
2 files changed, 166 insertions(+), 67 deletions(-)
diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py
index 5e921fd6e..b07100115 100644
--- a/.github/generate_changelog.py
+++ b/.github/generate_changelog.py
@@ -8,7 +8,8 @@ import re
import sys
import json
import argparse
-from typing import List, Tuple, Optional
+import os
+from typing import List, Tuple, Optional, Dict, Any
def run_git_command(args: List[str]) -> str:
@@ -30,14 +31,14 @@ def get_last_two_tags() -> Tuple[str, str]:
"""Get the latest two git tags."""
tags = run_git_command(["tag", "--sort=-version:refname"])
tag_list = [t for t in tags.split("\n") if t]
-
+
# Filter out old tags that start with 'v' (old versioning scheme)
tag_list = [t for t in tag_list if not t.startswith('v')]
if len(tag_list) < 2:
print("Error: Need at least 2 tags in the repository", file=sys.stderr)
sys.exit(1)
-
+
return tag_list[0], tag_list[1]
@@ -55,58 +56,144 @@ def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
return None
-def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]:
- """Get commits between two tags. Returns list of (message, author) tuples."""
+def get_repo_info() -> Tuple[str, str]:
+ """Get repository owner and name from git remote."""
+ try:
+ remote_url = run_git_command(["config", "--get", "remote.origin.url"])
+
+ # Handle both HTTPS and SSH URLs
+ # SSH: git@github.com:owner/repo.git
+ # HTTPS: https://github.com/owner/repo.git
+ match = re.search(r'github\.com[:/](.+?)/(.+?)(?:\.git)?$', remote_url)
+ if match:
+ owner = match.group(1)
+ repo = match.group(2)
+ return owner, repo
+ else:
+ print("Error: Could not parse GitHub repository from remote URL", file=sys.stderr)
+ sys.exit(1)
+ except:
+ print("Error: Could not get git remote URL", file=sys.stderr)
+ sys.exit(1)
+
+
+def get_commits_between_tags(tag1: str, tag2: str) -> List[str]:
+ """Get commit SHAs between two tags."""
log_output = run_git_command([
"log",
f"{tag2}..{tag1}",
- "--format=%s|%an|%h"
+ "--format=%H"
])
-
- commits = []
- for line in log_output.split("\n"):
- if "|" in line:
- message, author, sha = line.split("|", 2)
- commits.append((message.strip(), author.strip(), sha.strip()))
-
+
+ commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()]
return commits
-def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]:
- """Filter out commits matching any of the ignore patterns."""
+def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]:
+ """Get PR information for a commit using GitHub API."""
+ try:
+ import requests
+ except ImportError:
+ print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
+ sys.exit(1)
+
+ headers = {
+ "Accept": "application/vnd.github+json",
+ "X-GitHub-Api-Version": "2022-11-28"
+ }
+
+ if token:
+ headers["Authorization"] = f"Bearer {token}"
+
+ url = f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}/pulls"
+
+ try:
+ response = requests.get(url, headers=headers)
+ response.raise_for_status()
+ prs = response.json()
+
+ if prs and len(prs) > 0:
+ # Return the first PR (most relevant one)
+ pr = prs[0]
+ return {
+ "number": pr["number"],
+ "title": pr["title"],
+ "author": pr["user"]["login"],
+ "url": pr["html_url"]
+ }
+ except requests.exceptions.HTTPError as e:
+ if e.response.status_code == 404:
+ # Commit might not be associated with a PR
+ return None
+ elif e.response.status_code == 403:
+ print("Warning: GitHub API rate limit exceeded. Consider providing a token.", file=sys.stderr)
+ return None
+ else:
+ print(f"Warning: Failed to fetch PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
+ return None
+ except Exception as e:
+ print(f"Warning: Error fetching PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
+ return None
+
+ return None
+
+
+def get_prs_between_tags(tag1: str, tag2: str, owner: str, repo: str, token: str) -> List[Dict[str, Any]]:
+ """Get PRs between two tags using GitHub API."""
+ commits = get_commits_between_tags(tag1, tag2)
+ print(f"Found {len(commits)} commits, fetching PR information...")
+
+ prs = []
+ seen_pr_numbers = set()
+
+ for i, commit_sha in enumerate(commits, 1):
+ if i % 10 == 0:
+ print(f"Progress: {i}/{len(commits)} commits processed...")
+
+ pr_info = get_pr_for_commit(commit_sha, owner, repo, token)
+ if pr_info and pr_info["number"] not in seen_pr_numbers:
+ seen_pr_numbers.add(pr_info["number"])
+ prs.append(pr_info)
+
+ return prs
+
+
+def filter_prs(prs: List[Dict[str, Any]], ignore_patterns: List[str]) -> List[Dict[str, Any]]:
+ """Filter out PRs matching any of the ignore patterns."""
compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
-
+
filtered = []
- for message, author, sha in commits:
- if not any(pattern.search(message) for pattern in compiled_patterns):
- filtered.append((message, author, sha))
-
+ for pr in prs:
+ if not any(pattern.search(pr["title"]) for pattern in compiled_patterns):
+ filtered.append(pr)
+
return filtered
-def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]],
- cs_commit_new: Optional[str], cs_commit_old: Optional[str]) -> str:
+def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]],
+ cs_commit_new: Optional[str], cs_commit_old: Optional[str],
+ owner: str, repo: str) -> str:
"""Generate markdown changelog."""
# Calculate statistics
- commit_count = len(commits)
- unique_authors = len(set(author for _, author, _ in commits))
-
+ pr_count = len(prs)
+ unique_authors = len(set(pr["author"] for pr in prs))
+
changelog = f"# Dalamud Release v{version}\n\n"
changelog += f"We just released Dalamud v{version}, which should be available to users within a few minutes. "
- changelog += f"This release includes **{commit_count} commit{'s' if commit_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n"
- changelog += f"[Click here]() to see all Dalamud changes.\n\n"
-
+ changelog += f"This release includes **{pr_count} PR{'s' if pr_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n"
+ changelog += f"[Click here]() to see all Dalamud changes.\n\n"
+
if cs_commit_new and cs_commit_old and cs_commit_new != cs_commit_old:
changelog += f"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`]()**.\n"
changelog += f"[Click here]() to see all CS changes.\n"
elif cs_commit_new:
changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`]()**.\n"
-
+
changelog += "## Dalamud Changes\n\n"
-
- for message, author, sha in commits:
- changelog += f"* {message} (by **{author}** as [`{sha}`]())\n"
-
+
+ for pr in prs:
+ changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n"
+
return changelog
@@ -117,9 +204,9 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None:
except ImportError:
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
sys.exit(1)
-
+
filename = f"changelog-v{version}.md"
-
+
# Prepare the payload
data = {
"content": f"Dalamud v{version} has been released!",
@@ -130,13 +217,13 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None:
}
]
}
-
+
# Prepare the files
files = {
"payload_json": (None, json.dumps(data)),
"files[0]": (filename, content.encode('utf-8'), 'text/markdown')
}
-
+
try:
result = requests.post(webhook_url, files=files)
result.raise_for_status()
@@ -158,54 +245,64 @@ def main():
required=True,
help="Discord webhook URL"
)
+ parser.add_argument(
+ "--github-token",
+ default=os.environ.get("GITHUB_TOKEN"),
+ help="GitHub API token (or set GITHUB_TOKEN env var). Increases rate limit."
+ )
parser.add_argument(
"--ignore",
action="append",
default=[],
- help="Regex patterns to ignore commits (can be specified multiple times)"
+ help="Regex patterns to ignore PRs (can be specified multiple times)"
)
parser.add_argument(
"--submodule-path",
default="lib/FFXIVClientStructs",
help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)"
)
-
+
args = parser.parse_args()
-
+
+ # Get repository info
+ owner, repo = get_repo_info()
+ print(f"Repository: {owner}/{repo}")
+
# Get the last two tags
latest_tag, previous_tag = get_last_two_tags()
print(f"Generating changelog between {previous_tag} and {latest_tag}")
-
+
# Get submodule commits at both tags
cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag)
cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag)
-
+
if cs_commit_new:
print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}")
if cs_commit_old:
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
-
- # Get commits between tags
- commits = get_commits_between_tags(latest_tag, previous_tag)
- print(f"Found {len(commits)} commits")
-
- # Filter commits
- filtered_commits = filter_commits(commits, args.ignore)
- print(f"After filtering: {len(filtered_commits)} commits")
-
+
+ # Get PRs between tags
+ prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token)
+ prs.reverse()
+ print(f"Found {len(prs)} PRs")
+
+ # Filter PRs
+ filtered_prs = filter_prs(prs, args.ignore)
+ print(f"After filtering: {len(filtered_prs)} PRs")
+
# Generate changelog
- changelog = generate_changelog(latest_tag, previous_tag, filtered_commits,
- cs_commit_new, cs_commit_old)
-
+ changelog = generate_changelog(latest_tag, previous_tag, filtered_prs,
+ cs_commit_new, cs_commit_old, owner, repo)
+
print("\n" + "="*50)
print("Generated Changelog:")
print("="*50)
print(changelog)
print("="*50 + "\n")
-
+
# Post to Discord
post_to_discord(args.webhook_url, changelog, latest_tag)
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml
index 5fed3b1eb..e62d5f37c 100644
--- a/.github/workflows/generate-changelog.yml
+++ b/.github/workflows/generate-changelog.yml
@@ -6,41 +6,43 @@ on:
tags:
- '*'
+permissions: read-all
+
jobs:
generate-changelog:
runs-on: ubuntu-latest
-
+
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history and tags
submodules: true # Fetch submodules
-
+
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.14'
-
+
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
-
+
- name: Generate and post changelog
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GIT_TERMINAL_PROMPT: 0
run: |
python .github/generate_changelog.py \
--webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \
- --ignore "^Merge" \
- --ignore "^build:" \
- --ignore "^docs:"
- env:
- GIT_TERMINAL_PROMPT: 0
-
+ --ignore "Update ClientStructs" \
+ --ignore "^build:"
+
- name: Upload changelog as artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: changelog
path: changelog-*.md
- if-no-files-found: ignore
\ No newline at end of file
+ if-no-files-found: ignore
From 652ff59672efa8abf1a7cba81898431be4b61c81 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 7 Dec 2025 15:52:26 +0100
Subject: [PATCH 28/43] build: 13.0.0.13
---
Dalamud/Dalamud.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index d1f730d5e..a50f12d79 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -6,7 +6,7 @@
XIV Launcher addon framework
- 13.0.0.12
+ 13.0.0.13
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
From 2029a0f8a69e0d10b32b8795ce180ebf3c6eb8e5 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 7 Dec 2025 21:31:25 +0100
Subject: [PATCH 29/43] Also add fallback for SeStringDrawState.ScreenOffset
for now, make sure that it is populated
---
.../SeStringDrawParams.cs | 4 +++-
.../ImGuiSeStringRenderer/SeStringDrawState.cs | 3 ++-
.../Data/Widgets/SeStringRendererTestWidget.cs | 18 ++++++++++++++++++
3 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
index 972013328..09c3e9ed9 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs
@@ -25,7 +25,9 @@ public record struct SeStringDrawParams
public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; }
/// Gets or sets the screen offset of the left top corner.
- /// Screen offset to draw at, or null to use .
+ /// Screen offset to draw at, or null to use , if no
+ /// is specified. Otherwise, you must specify it (for example, by passing when passing the window
+ /// draw list.
public Vector2? ScreenOffset { get; set; }
/// Gets or sets the font to use.
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
index 5e63ef160..5601100e9 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -63,11 +63,12 @@ public unsafe ref struct SeStringDrawState
else
{
this.drawList = ssdp.TargetDrawList.Value;
- this.ScreenOffset = Vector2.Zero;
+ this.ScreenOffset = ssdp.ScreenOffset ?? Vector2.Zero;
// API14: Remove, always throw
if (ThreadSafety.IsMainThread)
{
+ this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos();
this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize();
}
else
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
index 0f51e0322..6a07152e5 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
@@ -177,6 +177,24 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGuiHelpers.SeStringWrapped(this.logkind.Value.Data.Span, this.style);
}
+ if (ImGui.CollapsingHeader("Draw into drawlist"))
+ {
+ ImGuiHelpers.ScaledDummy(100);
+ ImGui.SetCursorScreenPos(ImGui.GetItemRectMin() + ImGui.GetStyle().FramePadding);
+ var clipMin = ImGui.GetItemRectMin() + ImGui.GetStyle().FramePadding;
+ var clipMax = ImGui.GetItemRectMax() - ImGui.GetStyle().FramePadding;
+ clipMin.Y = MathF.Max(clipMin.Y, ImGui.GetWindowPos().Y);
+ clipMax.Y = MathF.Min(clipMax.Y, ImGui.GetWindowPos().Y + ImGui.GetWindowHeight());
+
+ var dl = ImGui.GetWindowDrawList();
+ dl.PushClipRect(clipMin, clipMax);
+ ImGuiHelpers.CompileSeStringWrapped(
+ "Test test",
+ new SeStringDrawParams
+ { Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = dl });
+ dl.PopClipRect();
+ }
+
if (ImGui.CollapsingHeader("Addon Table"u8))
{
if (ImGui.BeginTable("Addon Sheet"u8, 3))
From c45c6aafe1a8a8561826155aae0efc46b478a2d8 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 7 Dec 2025 21:57:54 +0100
Subject: [PATCH 30/43] Don't consider failed index integrity checks as having
"modified game data files"
---
Dalamud/Data/DataManager.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs
index ed0aa6c4d..559bd84dc 100644
--- a/Dalamud/Data/DataManager.cs
+++ b/Dalamud/Data/DataManager.cs
@@ -82,8 +82,10 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
var tsInfo =
JsonConvert.DeserializeObject(
dalamud.StartInfo.TroubleshootingPackData);
+
+ // Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout
this.HasModifiedGameDataFiles =
- tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
+ tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed;
if (this.HasModifiedGameDataFiles)
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);
From 8ed1af30dfa33c892fa1b0446054e512a4d0a760 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 7 Dec 2025 22:55:16 +0100
Subject: [PATCH 31/43] build: 13.0.0.14
---
Dalamud/Dalamud.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index a50f12d79..a4b203406 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -6,7 +6,7 @@
XIV Launcher addon framework
- 13.0.0.13
+ 13.0.0.14
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
From d0110f7251d50b2a45fbc51d6a8b03662e7afa54 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Mon, 8 Dec 2025 20:03:22 +0100
Subject: [PATCH 32/43] Hardcode HasModifiedGameDataFiles to false for now
until XL is fixed
---
Dalamud/Data/DataManager.cs | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs
index 559bd84dc..f53195a2d 100644
--- a/Dalamud/Data/DataManager.cs
+++ b/Dalamud/Data/DataManager.cs
@@ -84,8 +84,11 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
dalamud.StartInfo.TroubleshootingPackData);
// Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout
- this.HasModifiedGameDataFiles =
- tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed;
+ // this.HasModifiedGameDataFiles =
+ // tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed;
+
+ // TODO: Put above back when check in XL is fixed
+ this.HasModifiedGameDataFiles = false;
if (this.HasModifiedGameDataFiles)
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);
From 5d08170333d4e460b3eeb5197e286df86d5bdffe Mon Sep 17 00:00:00 2001
From: goaaats
Date: Mon, 8 Dec 2025 20:03:43 +0100
Subject: [PATCH 33/43] Keep rendering title bar buttons if one is not
available clickthrough
---
Dalamud/Interface/Windowing/Window.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index b0786fbb5..7ecd5e15c 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -864,7 +864,7 @@ public abstract class Window
foreach (var button in this.allButtons)
{
if (this.internalIsClickthrough && !button.AvailableClickthrough)
- return;
+ continue;
Vector2 position = new(titleBarRect.Max.X - padR - buttonSize, titleBarRect.Min.Y + style.FramePadding.Y);
padR += buttonSize + style.ItemInnerSpacing.X;
From 24caa1cb18f47c705b6706e718a645f3d25c056f Mon Sep 17 00:00:00 2001
From: goaaats
Date: Mon, 8 Dec 2025 20:05:14 +0100
Subject: [PATCH 34/43] PresetWindow.IsDefault can be JsonIgnore
---
Dalamud/Interface/Windowing/Persistence/PresetModel.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/Dalamud/Interface/Windowing/Persistence/PresetModel.cs b/Dalamud/Interface/Windowing/Persistence/PresetModel.cs
index f7910e0b2..4ddf55e51 100644
--- a/Dalamud/Interface/Windowing/Persistence/PresetModel.cs
+++ b/Dalamud/Interface/Windowing/Persistence/PresetModel.cs
@@ -53,6 +53,7 @@ internal class PresetModel
///
/// Gets a value indicating whether this preset is in the default state.
///
+ [JsonIgnore]
public bool IsDefault =>
!this.IsPinned &&
!this.IsClickThrough &&
From 2806e59dba4562e75f7721e40fd3d2bdd3935577 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Mon, 8 Dec 2025 20:09:31 +0100
Subject: [PATCH 35/43] Also remove borders for dev bar, to prevent themes from
causing weirdness
---
Dalamud/Interface/Internal/DalamudInterface.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index bf55a5486..b0fbeb6c5 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -669,6 +669,8 @@ internal class DalamudInterface : IInternalDisposableService
{
using var barColor = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0.060f, 0.060f, 0.060f, 0.773f));
barColor.Push(ImGuiCol.MenuBarBg, Vector4.Zero);
+ barColor.Push(ImGuiCol.Border, Vector4.Zero);
+ barColor.Push(ImGuiCol.BorderShadow, Vector4.Zero);
if (ImGui.BeginMainMenuBar())
{
var pluginManager = Service.Get();
From 97df73acea61ddf94c135bc37e4f402b6e5a6cab Mon Sep 17 00:00:00 2001
From: goaaats
Date: Mon, 8 Dec 2025 21:00:08 +0100
Subject: [PATCH 36/43] Ensure that we don't catch mouse up events without
corresponding mouse down events Fixes an issue wherein the cursor could get
locked by the game if WantCaptureMouse becomes true in between down and up
events
---
.../InputHandler/Win32InputHandler.cs | 38 ++++++++++++++++++-
1 file changed, 37 insertions(+), 1 deletion(-)
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
index 0b2e27b57..6b26ce37d 100644
--- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Bindings.ImGui;
+using Dalamud.Console;
using Dalamud.Memory;
using Dalamud.Utility;
@@ -37,6 +38,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private readonly WndProcDelegate wndProcDelegate;
private readonly nint platformNamePtr;
+ private readonly IConsoleVariable cvLogMouseEvents;
+
private ViewportHandler viewportHandler;
private int mouseButtonsDown;
@@ -87,6 +90,11 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.cursors[(int)ImGuiMouseCursor.ResizeNwse] = LoadCursorW(default, IDC.IDC_SIZENWSE);
this.cursors[(int)ImGuiMouseCursor.Hand] = LoadCursorW(default, IDC.IDC_HAND);
this.cursors[(int)ImGuiMouseCursor.NotAllowed] = LoadCursorW(default, IDC.IDC_NO);
+
+ this.cvLogMouseEvents = Service.Get().AddVariable(
+ "imgui.log_mouse_events",
+ "Log mouse events to console for debugging",
+ false);
}
///
@@ -267,11 +275,23 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_XBUTTONDOWN:
case WM.WM_XBUTTONDBLCLK:
{
+ if (this.cvLogMouseEvents.Value)
+ {
+ Log.Verbose(
+ "Handle MouseDown {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}",
+ GetButton(msg, wParam),
+ io.WantCaptureMouse,
+ this.mouseButtonsDown);
+ }
+
var button = GetButton(msg, wParam);
if (io.WantCaptureMouse)
{
if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero)
+ {
SetCapture(hWndCurrent);
+ }
+
this.mouseButtonsDown |= 1 << button;
io.AddMouseButtonEvent(button, true);
return default(LRESULT);
@@ -288,12 +308,28 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MBUTTONUP:
case WM.WM_XBUTTONUP:
{
+ if (this.cvLogMouseEvents.Value)
+ {
+ Log.Verbose(
+ "Handle MouseUp {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}",
+ GetButton(msg, wParam),
+ io.WantCaptureMouse,
+ this.mouseButtonsDown);
+ }
+
var button = GetButton(msg, wParam);
- if (io.WantCaptureMouse)
+
+ // Need to check if we captured the button event away from the game here, otherwise the game might get
+ // a down event but no up event, causing the cursor to get stuck.
+ // Can happen if WantCaptureMouse becomes true in between down and up
+ if (io.WantCaptureMouse && (this.mouseButtonsDown & (1 << button)) != 0)
{
this.mouseButtonsDown &= ~(1 << button);
if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent)
+ {
ReleaseCapture();
+ }
+
io.AddMouseButtonEvent(button, false);
return default(LRESULT);
}
From e53ccdbcc03a4931e4594491c6250395e5a3a3e5 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Tue, 9 Dec 2025 00:18:28 +0100
Subject: [PATCH 37/43] build: 13.0.0.15
---
Dalamud/Dalamud.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index a4b203406..5aca47e0c 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -6,7 +6,7 @@
XIV Launcher addon framework
- 13.0.0.14
+ 13.0.0.15
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
From b88a6bb61646d32bf2ca2df54b611f58a81a5e5b Mon Sep 17 00:00:00 2001
From: nebel <9887+nebel@users.noreply.github.com>
Date: Wed, 10 Dec 2025 23:12:44 +0900
Subject: [PATCH 38/43] Always pop DalamudStandard style if pushed earlier in
Draw
---
Dalamud/Interface/Windowing/Window.cs | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index 7ecd5e15c..ed9318e49 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -672,16 +672,13 @@ public abstract class Window
Task.FromResult(tex));
}
- if (!this.hasError)
+ if (isErrorStylePushed)
{
- this.PostDraw();
+ Style.StyleModelV1.DalamudStandard.Pop();
}
else
{
- if (isErrorStylePushed)
- {
- Style.StyleModelV1.DalamudStandard.Pop();
- }
+ this.PostDraw();
}
this.PostHandlePreset(persistence);
From 201c9cfcf25c578f3bb2a3964b8674708b35d634 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 10 Dec 2025 01:42:45 +0100
Subject: [PATCH 39/43] Use game window to calculate offsets in fallback mouse
position code
---
.../ImGuiBackend/InputHandler/Win32InputHandler.cs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
index 6b26ce37d..8417a90e5 100644
--- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
@@ -494,7 +494,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
// (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);
+ {
+ // Use game window, otherwise, positions are calculated based on the focused window which might not be the game.
+ // Leads to offsets.
+ ClientToScreen(this.hWnd, &mousePos);
+ }
+
io.AddMousePosEvent(mousePos.x, mousePos.y);
}
From a39763f161b893e5735b0dbe3bd7dde42bb38d5f Mon Sep 17 00:00:00 2001
From: goaaats
Date: Wed, 10 Dec 2025 18:32:18 +0100
Subject: [PATCH 40/43] Mark preset dirty when disabling clickthrough for a
window
---
Dalamud/Interface/Windowing/Window.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index ed9318e49..5a79a017a 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -84,7 +84,7 @@ public abstract class Window
Click = _ =>
{
this.internalIsClickthrough = false;
- this.presetDirty = false;
+ this.presetDirty = true;
ImGui.OpenPopup(AdditionsPopupName);
},
Priority = int.MinValue,
@@ -905,7 +905,7 @@ public abstract class Window
private void DrawErrorMessage()
{
// TODO: Once window systems are services, offer to reload the plugin
- ImGui.TextColoredWrapped(ImGuiColors.DalamudRed,Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details."));
+ ImGui.TextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details."));
ImGuiHelpers.ScaledDummy(5);
From 0b55dc3e10b1a43013fee7893ed3dd88762a2f60 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Thu, 11 Dec 2025 22:59:50 +0100
Subject: [PATCH 41/43] Clear ImDrawListSplitter when disposing
SeStringDrawState
---
.../ImGuiSeStringRenderer/Internal/SeStringRenderer.cs | 2 +-
.../Interface/ImGuiSeStringRenderer/SeStringDrawState.cs | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
index 397502b30..4a8e6517e 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs
@@ -170,7 +170,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.
- var stateStorage = new SeStringDrawState(
+ using 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 5601100e9..885508bed 100644
--- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
+++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs
@@ -10,6 +10,7 @@ using Dalamud.Interface.Utility;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
+
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
@@ -17,7 +18,7 @@ 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;
@@ -194,6 +195,9 @@ public unsafe ref struct SeStringDrawState
/// Gets the text fragments.
internal List Fragments { get; }
+ ///
+ public void Dispose() => this.splitter.ClearFreeMemory();
+
/// Sets the current channel in the ImGui draw list splitter.
/// Channel to switch to.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
From e100ec2abdc490838f3422bf5fd5cda695a886fd Mon Sep 17 00:00:00 2001
From: goaaats
Date: Fri, 12 Dec 2025 00:57:04 +0100
Subject: [PATCH 42/43] build: 13.0.0.16
---
Dalamud/Dalamud.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 5aca47e0c..3ec3c0865 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -6,7 +6,7 @@
XIV Launcher addon framework
- 13.0.0.15
+ 13.0.0.16
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
From 2d096d9b334ee485764a1e00589cba65c273af3e Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Sat, 13 Dec 2025 05:05:03 +0100
Subject: [PATCH 43/43] Properly initialize GameInventoryItems (#2504)
---
Dalamud/Game/Inventory/GameInventory.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs
index 535b84372..5390c2707 100644
--- a/Dalamud/Game/Inventory/GameInventory.cs
+++ b/Dalamud/Game/Inventory/GameInventory.cs
@@ -305,7 +305,8 @@ internal class GameInventory : IInternalDisposableService
private GameInventoryItem[] CreateItemsArray(int length)
{
var items = new GameInventoryItem[length];
- items.Initialize();
+ foreach (ref var item in items.AsSpan())
+ item = new();
return items;
}