From c19ea6ace38f79386f1c4727851dcbcdf23b036f Mon Sep 17 00:00:00 2001
From: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Date: Tue, 1 Jul 2025 19:02:32 +0900
Subject: [PATCH 001/477] 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 8134cde2b7888d2c801b05cd0a6f40326bdb045c Mon Sep 17 00:00:00 2001
From: bleatbot <106497096+bleatbot@users.noreply.github.com>
Date: Sun, 10 Aug 2025 20:31:52 +0200
Subject: [PATCH 002/477] Update ClientStructs (#2364)
Co-authored-by: github-actions[bot]
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index 9bb5998f4..1b1d6f85e 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit 9bb5998f410c189bacbad7423b7ce7dd9a3ce976
+Subproject commit 1b1d6f85ef38f2756797346e0f6fc9bd551b563c
From 4a62138b3091c563b757d4f8b63f9316f842e8f6 Mon Sep 17 00:00:00 2001
From: bleatbot <106497096+bleatbot@users.noreply.github.com>
Date: Mon, 11 Aug 2025 18:15:27 +0200
Subject: [PATCH 003/477] Update ClientStructs (#2365)
Co-authored-by: github-actions[bot]
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index 1b1d6f85e..6ae8ad159 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit 1b1d6f85ef38f2756797346e0f6fc9bd551b563c
+Subproject commit 6ae8ad159cac376e95a100025080772fa4f73580
From ae58aaff3088a73254b319972585908f125943ae Mon Sep 17 00:00:00 2001
From: marzent
Date: Mon, 11 Aug 2025 20:23:15 +0200
Subject: [PATCH 004/477] avoid c++ exceptions in import hook (#2367)
---
Dalamud.Boot/hooks.cpp | 43 +++++++++++++++++++++++++++++-------------
1 file changed, 30 insertions(+), 13 deletions(-)
diff --git a/Dalamud.Boot/hooks.cpp b/Dalamud.Boot/hooks.cpp
index 295d427ae..bb11572a1 100644
--- a/Dalamud.Boot/hooks.cpp
+++ b/Dalamud.Boot/hooks.cpp
@@ -82,21 +82,38 @@ void hooks::getprocaddress_singleton_import_hook::initialize() {
s_dllChanged = 1;
if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
const auto dllName = unicode::convert(pData->Loaded.FullDllName->Buffer);
+ std::wstring version = L"";
+ std::wstring description = L"";
+ DWORD versionSize = GetFileVersionInfoSizeA(dllName.c_str(), NULL);
- utils::loaded_module mod(pData->Loaded.DllBase);
- std::wstring version, description;
- try {
- version = utils::format_file_version(mod.get_file_version());
- } catch (...) {
- version = L"";
+ if (versionSize > 0) {
+ std::vector versionData(versionSize);
+ if (GetFileVersionInfoA(dllName.c_str(), 0, versionSize, versionData.data())) {
+ struct LANGANDCODEPAGE {
+ WORD wLanguage;
+ WORD wCodePage;
+ } *translate = nullptr;
+
+ UINT uLen = 0;
+ LPVOID lpBuffer;
+ if (VerQueryValueW(versionData.data(), L"\\VarFileInfo\\Translation", (LPVOID*)&translate, &uLen) && uLen >= sizeof(LANGANDCODEPAGE)) {
+ // Use the first language/codepage
+ wchar_t subBlock[256];
+ swprintf(subBlock, 256, L"\\StringFileInfo\\%04x%04x\\FileDescription", translate[0].wLanguage, translate[0].wCodePage);
+
+ if (VerQueryValueW(versionData.data(), subBlock, &lpBuffer, &uLen)) {
+ description = std::wstring((wchar_t *)lpBuffer, uLen - 1);
+ }
+
+ swprintf(subBlock, 256, L"\\StringFileInfo\\%04x%04x\\FileVersion", translate[0].wLanguage, translate[0].wCodePage);
+
+ if (VerQueryValueW(versionData.data(), subBlock, &lpBuffer, &uLen)) {
+ version = std::wstring((wchar_t*)lpBuffer, uLen - 1);
+ }
+ }
+ }
}
-
- try {
- description = mod.get_description();
- } catch (...) {
- description = L"";
- }
-
+
logging::I(R"({} "{}" ("{}" ver {}) has been loaded at 0x{:X} ~ 0x{:X} (0x{:X}); finding import table items to hook.)",
LogTag, dllName, description, version,
reinterpret_cast(pData->Loaded.DllBase),
From 6341640243f405e7fc0a86751da0e3e6837aabe3 Mon Sep 17 00:00:00 2001
From: KazWolfe
Date: Mon, 11 Aug 2025 12:51:00 -0700
Subject: [PATCH 005/477] feat: Add GitLab sync (#2312)
* feat: Add GitLab sync
* Add codeberg to sync script
---------
Co-authored-by: goat <16760685+goaaats@users.noreply.github.com>
---
.github/workflows/backup.yml | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 .github/workflows/backup.yml
diff --git a/.github/workflows/backup.yml b/.github/workflows/backup.yml
new file mode 100644
index 000000000..a581f8ca2
--- /dev/null
+++ b/.github/workflows/backup.yml
@@ -0,0 +1,30 @@
+name: Back up code to GitLab and other sources
+
+on:
+ schedule:
+ - cron: '0 2 * * *' # Run every day at 2 AM
+ workflow_dispatch: # Allow manual trigger
+
+jobs:
+ push-to-gitlab:
+ runs-on: ubuntu-latest
+ if: github.repository == 'goatcorp/Dalamud'
+
+ steps:
+ - name: Checkout the repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd #v0.9.1
+ with:
+ ssh-private-key: |
+ ${{ secrets.MIRROR_GITLAB_SYNC_KEY }}
+ ${{ secrets.MIRROR_CODEBERG_SYNC_KEY }}
+
+ - name: Add remotes & push
+ run: |
+ git remote add gitlab git@gitlab.com:goatcorp/Dalamud.git
+ git push gitlab --all --force
+ git remote add codeberg git@codeberg.org/goatcorp/Dalamud.git
+ git push codeberg --all --force
From 98d9bf3a931d2243977882dc71477822289ce5ab Mon Sep 17 00:00:00 2001
From: goat <16760685+goaaats@users.noreply.github.com>
Date: Mon, 11 Aug 2025 21:57:06 +0200
Subject: [PATCH 006/477] Disable strict host key checking
---
.github/workflows/backup.yml | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/backup.yml b/.github/workflows/backup.yml
index a581f8ca2..5f2163d06 100644
--- a/.github/workflows/backup.yml
+++ b/.github/workflows/backup.yml
@@ -1,4 +1,4 @@
-name: Back up code to GitLab and other sources
+name: Back up code to other forges
on:
schedule:
@@ -6,7 +6,7 @@ on:
workflow_dispatch: # Allow manual trigger
jobs:
- push-to-gitlab:
+ push-to-forges:
runs-on: ubuntu-latest
if: github.repository == 'goatcorp/Dalamud'
@@ -23,6 +23,8 @@ jobs:
${{ secrets.MIRROR_CODEBERG_SYNC_KEY }}
- name: Add remotes & push
+ env:
+ GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=accept-new"
run: |
git remote add gitlab git@gitlab.com:goatcorp/Dalamud.git
git push gitlab --all --force
From 86ad1de1812df84c68705a7559ebe959f444f4d2 Mon Sep 17 00:00:00 2001
From: goat <16760685+goaaats@users.noreply.github.com>
Date: Mon, 11 Aug 2025 21:58:25 +0200
Subject: [PATCH 007/477] Fix codeberg ssh endpoint
---
.github/workflows/backup.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/backup.yml b/.github/workflows/backup.yml
index 5f2163d06..366f4682b 100644
--- a/.github/workflows/backup.yml
+++ b/.github/workflows/backup.yml
@@ -28,5 +28,5 @@ jobs:
run: |
git remote add gitlab git@gitlab.com:goatcorp/Dalamud.git
git push gitlab --all --force
- git remote add codeberg git@codeberg.org/goatcorp/Dalamud.git
+ git remote add codeberg git@codeberg.org:goatcorp/Dalamud.git
git push codeberg --all --force
From 6337e165aae2a13417e68bbc47d932d321f1494c Mon Sep 17 00:00:00 2001
From: wolfcomp <4028289+wolfcomp@users.noreply.github.com>
Date: Tue, 12 Aug 2025 03:13:06 +0200
Subject: [PATCH 008/477] Update Directory.Build.props (#2368)
---
Directory.Build.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Build.props b/Directory.Build.props
index 61469232a..b5330709f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -10,7 +10,7 @@
- 6.5.0
+ 6.5.1
13.0.3
From f613b177a2bc58ee0378c68f989d0f2515aa8fc5 Mon Sep 17 00:00:00 2001
From: bleatbot <106497096+bleatbot@users.noreply.github.com>
Date: Tue, 12 Aug 2025 03:28:24 +0200
Subject: [PATCH 009/477] Update ClientStructs (#2369)
Co-authored-by: github-actions[bot]
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index 6ae8ad159..af8bb59e7 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit 6ae8ad159cac376e95a100025080772fa4f73580
+Subproject commit af8bb59e79a4f50191dc7a5fc67e86a2624c934f
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 010/477] 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 011/477] 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 9092e36b3366aa2348d54378afa57236abd3b0d8 Mon Sep 17 00:00:00 2001
From: srkizer
Date: Fri, 15 Aug 2025 16:02:32 +0900
Subject: [PATCH 012/477] Reduce usage of exceptions from Boot (#2373)
* wip
* make pretty
* Remove CRT version check from IM
* fix
* Simplify IsDebuggerPresent hook
---
Dalamud.Boot/Dalamud.Boot.rc | 32 +++
Dalamud.Boot/Dalamud.Boot.vcxproj | 13 +-
Dalamud.Boot/Dalamud.Boot.vcxproj.filters | 6 +
Dalamud.Boot/dllmain.cpp | 125 ++++++----
Dalamud.Boot/error_info.cpp | 26 +++
Dalamud.Boot/error_info.h | 42 ++++
Dalamud.Boot/hooks.cpp | 41 +---
Dalamud.Boot/pch.h | 1 +
Dalamud.Boot/resource.h | 13 +-
Dalamud.Boot/rewrite_entrypoint.cpp | 51 ++++-
Dalamud.Boot/utils.cpp | 139 +++++++----
Dalamud.Boot/utils.h | 26 ++-
Dalamud.Boot/veh.cpp | 27 ++-
Dalamud.Boot/xivfixes.cpp | 216 ++++++++----------
.../Dalamud.Injector.Boot.vcxproj | 8 +-
.../Interface/Internal/InterfaceManager.cs | 23 --
16 files changed, 494 insertions(+), 295 deletions(-)
create mode 100644 Dalamud.Boot/error_info.cpp
create mode 100644 Dalamud.Boot/error_info.h
diff --git a/Dalamud.Boot/Dalamud.Boot.rc b/Dalamud.Boot/Dalamud.Boot.rc
index b46e81caf..655df27e1 100644
--- a/Dalamud.Boot/Dalamud.Boot.rc
+++ b/Dalamud.Boot/Dalamud.Boot.rc
@@ -26,6 +26,38 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
RT_MANIFEST_THEMES RT_MANIFEST "themes.manifest"
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// String Table
+//
+
+STRINGTABLE
+BEGIN
+ IDS_APPNAME "Dalamud Boot"
+ IDS_MSVCRT_ACTION_OPENDOWNLOAD
+ "Download Microsoft Visual C++ Redistributable 2022\nExit the game and download the latest setup file from Microsoft."
+ IDS_MSVCRT_ACTION_IGNORE
+ "Ignore and Continue\nAttempt to continue with the currently installed version.\nDalamud or plugins may fail to load."
+ IDS_MSVCRT_DIALOG_MAININSTRUCTION
+ "Outdated Microsoft Visual C++ Redistributable"
+ IDS_MSVCRT_DIALOG_CONTENT
+ "The Microsoft Visual C++ Redistributable version detected on this computer (v{0}.{1}.{2}.{3}) is out of date and may not work with Dalamud."
+ IDS_MSVCRT_DOWNLOADURL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
+ IDS_INITIALIZEFAIL_ACTION_ABORT "Abort\nExit the game."
+ IDS_INITIALIZEFAIL_ACTION_CONTINUE
+ "Load game without Dalamud\nThe game will launch without Dalamud enabled."
+ IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION "Failed to load Dalamud."
+ IDS_INITIALIZEFAIL_DIALOG_CONTENT
+ "An error is preventing Dalamud from being loaded along with the game."
+END
+
+STRINGTABLE
+BEGIN
+ IDS_INITIALIZEFAIL_DIALOG_FOOTER
+ "Last operation: {0}\nHRESULT: 0x{1:08X}\nDescription: {2}"
+END
+
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj
index a15601af4..0a4a9c563 100644
--- a/Dalamud.Boot/Dalamud.Boot.vcxproj
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj
@@ -48,7 +48,7 @@
Level3
true
true
- stdcpp20
+ stdcpp23
pch.h
ProgramDatabase
CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
@@ -65,7 +65,7 @@
true
false
- MultiThreadedDebugDLL
+ MultiThreadedDebugDLL
_DEBUG;%(PreprocessorDefinitions)
Use
26812
@@ -80,7 +80,7 @@
true
true
- MultiThreadedDLL
+ MultiThreadedDLL
NDEBUG;%(PreprocessorDefinitions)
Use
26812
@@ -133,6 +133,10 @@
NotUsing
+
+ NotUsing
+ NotUsing
+
NotUsing
@@ -176,6 +180,7 @@
+
@@ -206,4 +211,4 @@
-
+
\ No newline at end of file
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
index 7c26b28ff..15e3eb8b3 100644
--- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
@@ -76,6 +76,9 @@
Dalamud.Boot DLL
+
+ Common Boot
+
@@ -146,6 +149,9 @@
Dalamud.Boot DLL
+
+ Common Boot
+
diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp
index eee3953ad..80a16f89a 100644
--- a/Dalamud.Boot/dllmain.cpp
+++ b/Dalamud.Boot/dllmain.cpp
@@ -9,11 +9,12 @@
#include "utils.h"
#include "veh.h"
#include "xivfixes.h"
+#include "resource.h"
HMODULE g_hModule;
HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
-void CheckMsvcrtVersion() {
+static void CheckMsvcrtVersion() {
// Commit introducing inline mutex ctor: tagged vs-2022-17.14 (2024-06-18)
// - https://github.com/microsoft/STL/commit/22a88260db4d754bbc067e2002430144d6ec5391
// MSVC Redist versions:
@@ -28,67 +29,102 @@ void CheckMsvcrtVersion() {
| (static_cast(RequiredMsvcrtVersionComponents[2]) << 16)
| (static_cast(RequiredMsvcrtVersionComponents[3]) << 0);
-#ifdef _DEBUG
constexpr const wchar_t* RuntimeDllNames[] = {
+#ifdef _DEBUG
L"msvcp140d.dll",
L"vcruntime140d.dll",
L"vcruntime140_1d.dll",
- };
#else
- constexpr const wchar_t* RuntimeDllNames[] = {
L"msvcp140.dll",
L"vcruntime140.dll",
L"vcruntime140_1.dll",
- };
#endif
+ };
uint64_t lowestVersion = 0;
for (const auto& runtimeDllName : RuntimeDllNames) {
const utils::loaded_module mod(GetModuleHandleW(runtimeDllName));
if (!mod) {
- logging::E("Runtime DLL not found: {}", runtimeDllName);
+ logging::E("MSVCRT DLL not found: {}", runtimeDllName);
continue;
}
- try {
- const auto& versionFull = mod.get_file_version();
- logging::I("Runtime DLL {} has version {}.", runtimeDllName, utils::format_file_version(versionFull));
+ const auto path = mod.path()
+ .transform([](const auto& p) { return p.wstring(); })
+ .value_or(runtimeDllName);
- const auto version = (static_cast(versionFull.dwFileVersionMS) << 32) |
- static_cast(versionFull.dwFileVersionLS);
+ if (const auto versionResult = mod.get_file_version()) {
+ const auto& versionFull = versionResult->get();
+ logging::I("MSVCRT DLL {} has version {}.", path, utils::format_file_version(versionFull));
+
+ const auto version = 0ULL |
+ (static_cast(versionFull.dwFileVersionMS) << 32) |
+ (static_cast(versionFull.dwFileVersionLS) << 0);
if (version < RequiredMsvcrtVersion && (lowestVersion == 0 || lowestVersion > version))
lowestVersion = version;
- } catch (const std::exception& e) {
- logging::E("Failed to detect Runtime DLL version for {}: {}", runtimeDllName, e.what());
+ } else {
+ logging::E("Failed to detect MSVCRT DLL version for {}: {}", path, versionResult.error().describe());
}
}
- if (lowestVersion) {
- switch (MessageBoxW(
- nullptr,
- L"Microsoft Visual C++ Redistributable should be updated, or Dalamud may not work as expected."
- L" Do you want to download and install the latest version from Microsoft?"
- L"\n"
- L"\n* Clicking \"Yes\" will exit the game and open the download page from Microsoft."
- L"\n* Clicking \"No\" will continue loading the game with Dalamud. This may fail."
- L"\n"
- L"\nClick \"X64\" from the table in the download page, regardless of what CPU you have.",
- L"Dalamud",
- MB_YESNO | MB_ICONWARNING)) {
- case IDYES:
- ShellExecuteW(
- nullptr,
- L"open",
- L"https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version",
- nullptr,
- nullptr,
- SW_SHOW);
- ExitProcess(0);
- break;
- case IDNO:
- break;
- }
+ if (!lowestVersion)
+ return;
+
+ enum IdTaskDialogAction {
+ IdTaskDialogActionOpenDownload = 101,
+ IdTaskDialogActionIgnore,
+ };
+
+ const TASKDIALOG_BUTTON buttons[]{
+ {IdTaskDialogActionOpenDownload, MAKEINTRESOURCEW(IDS_MSVCRT_ACTION_OPENDOWNLOAD)},
+ {IdTaskDialogActionIgnore, MAKEINTRESOURCEW(IDS_MSVCRT_ACTION_IGNORE)},
+ };
+
+ const WORD lowestVersionComponents[]{
+ static_cast(lowestVersion >> 48),
+ static_cast(lowestVersion >> 32),
+ static_cast(lowestVersion >> 16),
+ static_cast(lowestVersion >> 0),
+ };
+
+ const auto dialogContent = std::vformat(
+ utils::get_string_resource(IDS_MSVCRT_DIALOG_CONTENT),
+ std::make_wformat_args(
+ lowestVersionComponents[0],
+ lowestVersionComponents[1],
+ lowestVersionComponents[2],
+ lowestVersionComponents[3]));
+
+ const TASKDIALOGCONFIG config{
+ .cbSize = sizeof config,
+ .hInstance = g_hModule,
+ .dwFlags = TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS,
+ .pszWindowTitle = MAKEINTRESOURCEW(IDS_APPNAME),
+ .pszMainIcon = MAKEINTRESOURCEW(IDI_ICON1),
+ .pszMainInstruction = MAKEINTRESOURCEW(IDS_MSVCRT_DIALOG_MAININSTRUCTION),
+ .pszContent = dialogContent.c_str(),
+ .cButtons = _countof(buttons),
+ .pButtons = buttons,
+ .nDefaultButton = IdTaskDialogActionOpenDownload,
+ };
+
+ int buttonPressed;
+ if (utils::scoped_dpi_awareness_context ctx;
+ FAILED(TaskDialogIndirect(&config, &buttonPressed, nullptr, nullptr)))
+ buttonPressed = IdTaskDialogActionOpenDownload;
+
+ switch (buttonPressed) {
+ case IdTaskDialogActionOpenDownload:
+ ShellExecuteW(
+ nullptr,
+ L"open",
+ utils::get_string_resource(IDS_MSVCRT_DOWNLOADURL).c_str(),
+ nullptr,
+ nullptr,
+ SW_SHOW);
+ ExitProcess(0);
+ break;
}
}
@@ -103,7 +139,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
}
if (g_startInfo.BootShowConsole)
- ConsoleSetup(L"Dalamud Boot");
+ ConsoleSetup(utils::get_string_resource(IDS_APPNAME).c_str());
logging::update_dll_load_status(true);
@@ -240,7 +276,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
if (minHookLoaded) {
logging::I("Applying fixes...");
- xivfixes::apply_all(true);
+ std::thread([] { xivfixes::apply_all(true); }).join();
logging::I("Fixes OK");
} else {
logging::W("Skipping fixes, as MinHook has failed to load.");
@@ -251,11 +287,14 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
while (!IsDebuggerPresent())
Sleep(100);
logging::I("Debugger attached.");
+ __debugbreak();
}
- const auto fs_module_path = utils::get_module_path(g_hModule);
- const auto runtimeconfig_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.runtimeconfig.json").wstring();
- const auto module_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.dll").wstring();
+ const auto fs_module_path = utils::loaded_module(g_hModule).path();
+ if (!fs_module_path)
+ return fs_module_path.error();
+ const auto runtimeconfig_path = std::filesystem::path(*fs_module_path).replace_filename(L"Dalamud.runtimeconfig.json").wstring();
+ const auto module_path = std::filesystem::path(*fs_module_path).replace_filename(L"Dalamud.dll").wstring();
// ============================== CLR ========================================= //
diff --git a/Dalamud.Boot/error_info.cpp b/Dalamud.Boot/error_info.cpp
new file mode 100644
index 000000000..02356b730
--- /dev/null
+++ b/Dalamud.Boot/error_info.cpp
@@ -0,0 +1,26 @@
+#include "error_info.h"
+
+#define WIN32_LEAN_AND_MEAN
+#include
+
+DalamudBootError::DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription, long hresult) noexcept
+ : m_dalamudErrorDescription(dalamudErrorDescription)
+ , m_hresult(hresult) {
+}
+
+DalamudBootError::DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription) noexcept
+ : DalamudBootError(dalamudErrorDescription, E_FAIL) {
+}
+
+const char* DalamudBootError::describe() const {
+ switch (m_dalamudErrorDescription) {
+ case DalamudBootErrorDescription::ModuleResourceLoadFail:
+ return "Failed to load resource.";
+ case DalamudBootErrorDescription::ModuleResourceVersionReadFail:
+ return "Failed to query version information.";
+ case DalamudBootErrorDescription::ModuleResourceVersionSignatureFail:
+ return "Invalid version info found.";
+ default:
+ return "(unavailable)";
+ }
+}
diff --git a/Dalamud.Boot/error_info.h b/Dalamud.Boot/error_info.h
new file mode 100644
index 000000000..b5862d0dd
--- /dev/null
+++ b/Dalamud.Boot/error_info.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include
+#include
+
+typedef unsigned long DWORD;
+typedef _Return_type_success_(return >= 0) long HRESULT;
+
+enum class DalamudBootErrorDescription {
+ None,
+ ModulePathResolutionFail,
+ ModuleResourceLoadFail,
+ ModuleResourceVersionReadFail,
+ ModuleResourceVersionSignatureFail,
+};
+
+class DalamudBootError {
+ DalamudBootErrorDescription m_dalamudErrorDescription;
+ long m_hresult;
+
+public:
+ DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription, long hresult) noexcept;
+ DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription) noexcept;
+
+ const char* describe() const;
+
+ operator HRESULT() const {
+ return m_hresult;
+ }
+};
+
+template
+using DalamudExpected = std::expected<
+ std::conditional_t<
+ std::is_reference_v,
+ std::reference_wrapper>,
+ T
+ >,
+ DalamudBootError
+>;
+
+using DalamudUnexpected = std::unexpected;
diff --git a/Dalamud.Boot/hooks.cpp b/Dalamud.Boot/hooks.cpp
index bb11572a1..3443a5f8a 100644
--- a/Dalamud.Boot/hooks.cpp
+++ b/Dalamud.Boot/hooks.cpp
@@ -82,37 +82,14 @@ void hooks::getprocaddress_singleton_import_hook::initialize() {
s_dllChanged = 1;
if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
const auto dllName = unicode::convert(pData->Loaded.FullDllName->Buffer);
- std::wstring version = L"";
- std::wstring description = L"";
- DWORD versionSize = GetFileVersionInfoSizeA(dllName.c_str(), NULL);
- if (versionSize > 0) {
- std::vector versionData(versionSize);
- if (GetFileVersionInfoA(dllName.c_str(), 0, versionSize, versionData.data())) {
- struct LANGANDCODEPAGE {
- WORD wLanguage;
- WORD wCodePage;
- } *translate = nullptr;
+ utils::loaded_module mod(pData->Loaded.DllBase);
+ const auto version = mod.get_file_version()
+ .transform([](const auto& v) { return utils::format_file_version(v.get()); })
+ .value_or(L"");
- UINT uLen = 0;
- LPVOID lpBuffer;
- if (VerQueryValueW(versionData.data(), L"\\VarFileInfo\\Translation", (LPVOID*)&translate, &uLen) && uLen >= sizeof(LANGANDCODEPAGE)) {
- // Use the first language/codepage
- wchar_t subBlock[256];
- swprintf(subBlock, 256, L"\\StringFileInfo\\%04x%04x\\FileDescription", translate[0].wLanguage, translate[0].wCodePage);
-
- if (VerQueryValueW(versionData.data(), subBlock, &lpBuffer, &uLen)) {
- description = std::wstring((wchar_t *)lpBuffer, uLen - 1);
- }
-
- swprintf(subBlock, 256, L"\\StringFileInfo\\%04x%04x\\FileVersion", translate[0].wLanguage, translate[0].wCodePage);
-
- if (VerQueryValueW(versionData.data(), subBlock, &lpBuffer, &uLen)) {
- version = std::wstring((wchar_t*)lpBuffer, uLen - 1);
- }
- }
- }
- }
+ const auto description = mod.get_description()
+ .value_or(L"");
logging::I(R"({} "{}" ("{}" ver {}) has been loaded at 0x{:X} ~ 0x{:X} (0x{:X}); finding import table items to hook.)",
LogTag, dllName, description, version,
@@ -142,7 +119,9 @@ void hooks::getprocaddress_singleton_import_hook::hook_module(const utils::loade
if (mod.is_current_process())
return;
- const auto path = unicode::convert(mod.path().wstring());
+ const auto path = mod.path()
+ .transform([](const auto& p) { return unicode::convert(p.wstring()); })
+ .value_or("");
for (const auto& [hModule, targetFns] : m_targetFns) {
for (const auto& [targetFn, pfnThunk] : targetFns) {
@@ -150,7 +129,7 @@ void hooks::getprocaddress_singleton_import_hook::hook_module(const utils::loade
if (void* pGetProcAddressImport; mod.find_imported_function_pointer(dllName.c_str(), targetFn.c_str(), 0, pGetProcAddressImport)) {
auto& hook = m_hooks[hModule][targetFn][mod];
if (!hook) {
- logging::I("{} Hooking {}!{} imported by {}", LogTag, dllName, targetFn, unicode::convert(mod.path().wstring()));
+ logging::I("{} Hooking {}!{} imported by {}", LogTag, dllName, targetFn, path);
hook.emplace(std::format("getprocaddress_singleton_import_hook::hook_module({}!{})", dllName, targetFn), static_cast(pGetProcAddressImport), pfnThunk);
}
diff --git a/Dalamud.Boot/pch.h b/Dalamud.Boot/pch.h
index e46927f76..ac1394e57 100644
--- a/Dalamud.Boot/pch.h
+++ b/Dalamud.Boot/pch.h
@@ -55,6 +55,7 @@
#include
#include
#include
+#include
#include
// https://www.akenotsuki.com/misc/srell/en/
diff --git a/Dalamud.Boot/resource.h b/Dalamud.Boot/resource.h
index 51acf37df..2a1cde6e2 100644
--- a/Dalamud.Boot/resource.h
+++ b/Dalamud.Boot/resource.h
@@ -3,12 +3,23 @@
// Used by Dalamud.Boot.rc
//
#define IDI_ICON1 101
+#define IDS_APPNAME 102
+#define IDS_MSVCRT_ACTION_OPENDOWNLOAD 103
+#define IDS_MSVCRT_ACTION_IGNORE 104
+#define IDS_MSVCRT_DIALOG_MAININSTRUCTION 105
+#define IDS_MSVCRT_DIALOG_CONTENT 106
+#define IDS_MSVCRT_DOWNLOADURL 107
+#define IDS_INITIALIZEFAIL_ACTION_ABORT 108
+#define IDS_INITIALIZEFAIL_ACTION_CONTINUE 109
+#define IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION 110
+#define IDS_INITIALIZEFAIL_DIALOG_CONTENT 111
+#define IDS_INITIALIZEFAIL_DIALOG_FOOTER 112
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
-#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_RESOURCE_VALUE 103
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp
index 3a1672af7..69a4ec818 100644
--- a/Dalamud.Boot/rewrite_entrypoint.cpp
+++ b/Dalamud.Boot/rewrite_entrypoint.cpp
@@ -2,6 +2,7 @@
#include "logging.h"
#include "utils.h"
+#include "resource.h"
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
@@ -379,12 +380,50 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara
auto desc = err.Description();
if (desc.length() == 0)
desc = err.ErrorMessage();
- if (MessageBoxW(nullptr, std::format(
- L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n{}",
- last_operation,
- desc.GetBSTR()).c_str(),
- L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO)
- ExitProcess(-1);
+
+ enum IdTaskDialogAction {
+ IdTaskDialogActionAbort = 101,
+ IdTaskDialogActionContinue,
+ };
+
+ const TASKDIALOG_BUTTON buttons[]{
+ {IdTaskDialogActionAbort, MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_ACTION_ABORT)},
+ {IdTaskDialogActionContinue, MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_ACTION_CONTINUE)},
+ };
+
+ const auto hru32 = static_cast(hr);
+ const auto footer = std::vformat(
+ utils::get_string_resource(IDS_INITIALIZEFAIL_DIALOG_FOOTER),
+ std::make_wformat_args(
+ last_operation,
+ hru32,
+ desc.GetBSTR()));
+
+ const TASKDIALOGCONFIG config{
+ .cbSize = sizeof config,
+ .hInstance = g_hModule,
+ .dwFlags = TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS | TDF_EXPAND_FOOTER_AREA,
+ .pszWindowTitle = MAKEINTRESOURCEW(IDS_APPNAME),
+ .pszMainIcon = MAKEINTRESOURCEW(IDI_ICON1),
+ .pszMainInstruction = MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION),
+ .pszContent = MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_DIALOG_CONTENT),
+ .cButtons = _countof(buttons),
+ .pButtons = buttons,
+ .nDefaultButton = IdTaskDialogActionAbort,
+ .pszFooter = footer.c_str(),
+ };
+
+ int buttonPressed;
+ if (utils::scoped_dpi_awareness_context ctx;
+ FAILED(TaskDialogIndirect(&config, &buttonPressed, nullptr, nullptr)))
+ buttonPressed = IdTaskDialogActionAbort;
+
+ switch (buttonPressed) {
+ case IdTaskDialogActionAbort:
+ ExitProcess(-1);
+ break;
+ }
+
if (hMainThreadContinue) {
CloseHandle(hMainThreadContinue);
hMainThreadContinue = nullptr;
diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp
index 91be2fb31..9820e5b7f 100644
--- a/Dalamud.Boot/utils.cpp
+++ b/Dalamud.Boot/utils.cpp
@@ -3,22 +3,27 @@
#include "utils.h"
-std::filesystem::path utils::loaded_module::path() const {
- std::wstring buf(MAX_PATH, L'\0');
- for (;;) {
- if (const auto len = GetModuleFileNameExW(GetCurrentProcess(), m_hModule, &buf[0], static_cast(buf.size())); len != buf.size()) {
- if (buf.empty())
- throw std::runtime_error(std::format("Failed to resolve module path: Win32 error {}", GetLastError()));
+DalamudExpected utils::loaded_module::path() const {
+ for (std::wstring buf(MAX_PATH, L'\0');; buf.resize(buf.size() * 2)) {
+ if (const auto len = GetModuleFileNameW(m_hModule, &buf[0], static_cast(buf.size()));
+ len != buf.size()) {
+ if (!len) {
+ return DalamudUnexpected(
+ std::in_place,
+ DalamudBootErrorDescription::ModulePathResolutionFail,
+ HRESULT_FROM_WIN32(GetLastError()));
+ }
+
buf.resize(len);
return buf;
}
- if (buf.size() * 2 < PATHCCH_MAX_CCH)
- buf.resize(buf.size() * 2);
- else if (auto p = std::filesystem::path(buf); exists(p))
- return p;
- else
- throw std::runtime_error("Failed to resolve module path: no amount of buffer size would fit the data");
+ if (buf.size() > PATHCCH_MAX_CCH) {
+ return DalamudUnexpected(
+ std::in_place,
+ DalamudBootErrorDescription::ModulePathResolutionFail,
+ E_OUTOFMEMORY);
+ }
}
}
@@ -144,21 +149,24 @@ void* utils::loaded_module::get_imported_function_pointer(const char* pcszDllNam
throw std::runtime_error(std::format("Failed to find import for {}!{} ({}).", pcszDllName, pcszFunctionName ? pcszFunctionName : "", hintOrOrdinal));
}
-std::unique_ptr, decltype(&FreeResource)> utils::loaded_module::get_resource(LPCWSTR lpName, LPCWSTR lpType) const {
+DalamudExpected, decltype(&FreeResource)>> utils::loaded_module::get_resource(LPCWSTR lpName, LPCWSTR lpType) const {
const auto hres = FindResourceW(m_hModule, lpName, lpType);
if (!hres)
- throw std::runtime_error("No such resource");
+ return DalamudUnexpected(std::in_place, DalamudBootErrorDescription::ModuleResourceLoadFail, GetLastError());
const auto hRes = LoadResource(m_hModule, hres);
if (!hRes)
- throw std::runtime_error("LoadResource failure");
+ return DalamudUnexpected(std::in_place, DalamudBootErrorDescription::ModuleResourceLoadFail, GetLastError());
- return {hRes, &FreeResource};
+ return std::unique_ptr, decltype(&FreeResource)>(hRes, &FreeResource);
}
-std::wstring utils::loaded_module::get_description() const {
- const auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
- const auto pBlock = LockResource(rsrc.get());
+DalamudExpected utils::loaded_module::get_description() const {
+ auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
+ if (!rsrc)
+ return DalamudUnexpected(std::move(rsrc.error()));
+
+ const auto pBlock = LockResource(rsrc->get());
struct LANGANDCODEPAGE {
WORD wLanguage;
@@ -166,44 +174,65 @@ std::wstring utils::loaded_module::get_description() const {
} * lpTranslate;
UINT cbTranslate;
if (!VerQueryValueW(pBlock,
- TEXT("\\VarFileInfo\\Translation"),
+ L"\\VarFileInfo\\Translation",
reinterpret_cast(&lpTranslate),
&cbTranslate)) {
- throw std::runtime_error("Invalid version information (1)");
+ return DalamudUnexpected(
+ std::in_place,
+ DalamudBootErrorDescription::ModuleResourceVersionReadFail,
+ HRESULT_FROM_WIN32(GetLastError()));
}
for (size_t i = 0; i < (cbTranslate / sizeof(LANGANDCODEPAGE)); i++) {
+ wchar_t subblockNameBuf[64];
+ *std::format_to_n(
+ subblockNameBuf,
+ _countof(subblockNameBuf),
+ L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
+ lpTranslate[i].wLanguage,
+ lpTranslate[i].wCodePage).out = 0;;
+
wchar_t* buf = nullptr;
UINT size = 0;
- if (!VerQueryValueW(pBlock,
- std::format(L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
- lpTranslate[i].wLanguage,
- lpTranslate[i].wCodePage).c_str(),
- reinterpret_cast(&buf),
- &size)) {
+ if (!VerQueryValueW(pBlock, subblockNameBuf, reinterpret_cast(&buf), &size))
continue;
- }
+
auto currName = std::wstring_view(buf, size);
- while (!currName.empty() && currName.back() == L'\0')
- currName = currName.substr(0, currName.size() - 1);
+ if (const auto p = currName.find(L'\0'); p != std::string::npos)
+ currName = currName.substr(0, p);
if (currName.empty())
continue;
return std::wstring(currName);
}
- throw std::runtime_error("Invalid version information (2)");
+ return DalamudUnexpected(
+ std::in_place,
+ DalamudBootErrorDescription::ModuleResourceVersionReadFail,
+ HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
}
-const VS_FIXEDFILEINFO& utils::loaded_module::get_file_version() const {
- const auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
- const auto pBlock = LockResource(rsrc.get());
+std::expected, DalamudBootError> utils::loaded_module::get_file_version() const {
+ auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
+ if (!rsrc)
+ return DalamudUnexpected(std::move(rsrc.error()));
+
+ const auto pBlock = LockResource(rsrc->get());
UINT size = 0;
LPVOID lpBuffer = nullptr;
- if (!VerQueryValueW(pBlock, L"\\", &lpBuffer, &size))
- throw std::runtime_error("Failed to query version information.");
+ if (!VerQueryValueW(pBlock, L"\\", &lpBuffer, &size)) {
+ return std::unexpected(
+ std::in_place,
+ DalamudBootErrorDescription::ModuleResourceVersionReadFail,
+ HRESULT_FROM_WIN32(GetLastError()));
+ }
+
const VS_FIXEDFILEINFO& versionInfo = *static_cast(lpBuffer);
- if (versionInfo.dwSignature != 0xfeef04bd)
- throw std::runtime_error("Invalid version info found.");
+ if (versionInfo.dwSignature != 0xfeef04bd) {
+ return std::unexpected(
+ std::in_place,
+ DalamudBootErrorDescription::ModuleResourceVersionSignatureFail);
+ }
+
return versionInfo;
}
@@ -589,17 +618,10 @@ bool utils::is_running_on_wine() {
return g_startInfo.Platform != "WINDOWS";
}
-std::filesystem::path utils::get_module_path(HMODULE hModule) {
- std::wstring buf(MAX_PATH, L'\0');
- while (true) {
- if (const auto res = GetModuleFileNameW(hModule, &buf[0], static_cast(buf.size())); !res)
- throw std::runtime_error(std::format("GetModuleFileName failure: 0x{:X}", GetLastError()));
- else if (res < buf.size()) {
- buf.resize(res);
- return buf;
- } else
- buf.resize(buf.size() * 2);
- }
+std::wstring utils::get_string_resource(uint32_t resId) {
+ LPCWSTR pstr;
+ const auto len = LoadStringW(g_hModule, resId, reinterpret_cast(&pstr), 0);
+ return std::wstring(pstr, len);
}
HWND utils::try_find_game_window() {
@@ -677,3 +699,22 @@ std::wstring utils::format_win32_error(DWORD err) {
return std::format(L"Win32 error ({}=0x{:X})", err, err);
}
+
+utils::scoped_dpi_awareness_context::scoped_dpi_awareness_context()
+ : scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) {
+}
+
+utils::scoped_dpi_awareness_context::scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT context) {
+ const auto user32 = GetModuleHandleW(L"user32.dll");
+ m_setThreadDpiAwarenessContext =
+ user32
+ ? reinterpret_cast(
+ GetProcAddress(user32, "SetThreadDpiAwarenessContext"))
+ : nullptr;
+ m_old = m_setThreadDpiAwarenessContext ? m_setThreadDpiAwarenessContext(context) : DPI_AWARENESS_CONTEXT_UNAWARE;
+}
+
+utils::scoped_dpi_awareness_context::~scoped_dpi_awareness_context() {
+ if (m_setThreadDpiAwarenessContext)
+ m_setThreadDpiAwarenessContext(m_old);
+}
diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h
index cbbbccee8..c5833722b 100644
--- a/Dalamud.Boot/utils.h
+++ b/Dalamud.Boot/utils.h
@@ -1,5 +1,6 @@
#pragma once
+#include
#include
#include
#include
@@ -7,6 +8,7 @@
#include
#include
+#include "error_info.h"
#include "unicode.h"
namespace utils {
@@ -18,7 +20,7 @@ namespace utils {
loaded_module(void* hModule) : m_hModule(reinterpret_cast(hModule)) {}
loaded_module(size_t hModule) : m_hModule(reinterpret_cast(hModule)) {}
- std::filesystem::path path() const;
+ DalamudExpected path() const;
bool is_current_process() const { return m_hModule == GetModuleHandleW(nullptr); }
bool owns_address(const void* pAddress) const;
@@ -57,9 +59,9 @@ namespace utils {
void* get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) const;
template TFn** get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) { return reinterpret_cast(get_imported_function_pointer(pcszDllName, pcszFunctionName, hintOrOrdinal)); }
- [[nodiscard]] std::unique_ptr, decltype(&FreeResource)> get_resource(LPCWSTR lpName, LPCWSTR lpType) const;
- [[nodiscard]] std::wstring get_description() const;
- [[nodiscard]] const VS_FIXEDFILEINFO& get_file_version() const;
+ [[nodiscard]] DalamudExpected, decltype(&FreeResource)>> get_resource(LPCWSTR lpName, LPCWSTR lpType) const;
+ [[nodiscard]] DalamudExpected get_description() const;
+ [[nodiscard]] DalamudExpected get_file_version() const;
static loaded_module current_process();
static std::vector all_modules();
@@ -268,7 +270,7 @@ namespace utils {
bool is_running_on_wine();
- std::filesystem::path get_module_path(HMODULE hModule);
+ std::wstring get_string_resource(uint32_t resId);
/// @brief Find the game main window.
/// @return Handle to the game main window, or nullptr if it doesn't exist (yet).
@@ -279,4 +281,18 @@ namespace utils {
std::wstring escape_shell_arg(const std::wstring& arg);
std::wstring format_win32_error(DWORD err);
+
+ class scoped_dpi_awareness_context {
+ DPI_AWARENESS_CONTEXT m_old;
+ decltype(&SetThreadDpiAwarenessContext) m_setThreadDpiAwarenessContext;
+
+ public:
+ scoped_dpi_awareness_context();
+ scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT);
+ ~scoped_dpi_awareness_context();
+ scoped_dpi_awareness_context(const scoped_dpi_awareness_context&) = delete;
+ scoped_dpi_awareness_context(scoped_dpi_awareness_context&&) = delete;
+ scoped_dpi_awareness_context& operator=(const scoped_dpi_awareness_context&) = delete;
+ scoped_dpi_awareness_context& operator=(scoped_dpi_awareness_context&&) = delete;
+ };
}
diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp
index 85d58eb9d..b0ec1cefa 100644
--- a/Dalamud.Boot/veh.cpp
+++ b/Dalamud.Boot/veh.cpp
@@ -102,9 +102,13 @@ bool is_ffxiv_address(const wchar_t* module_name, const DWORD64 address)
return false;
}
-static void append_injector_launch_args(std::vector& args)
+static DalamudExpected append_injector_launch_args(std::vector& args)
{
- args.emplace_back(L"--game=\"" + utils::loaded_module::current_process().path().wstring() + L"\"");
+ if (auto path = utils::loaded_module::current_process().path())
+ args.emplace_back(L"--game=\"" + path->wstring() + L"\"");
+ else
+ return DalamudUnexpected(std::in_place, std::move(path.error()));
+
switch (g_startInfo.DalamudLoadMethod) {
case DalamudStartInfo::LoadMethod::Entrypoint:
args.emplace_back(L"--mode=entrypoint");
@@ -155,6 +159,8 @@ static void append_injector_launch_args(std::vector& args)
args.emplace_back(szArgList[i]);
LocalFree(szArgList);
}
+
+ return {};
}
LONG exception_handler(EXCEPTION_POINTERS* ex)
@@ -358,11 +364,20 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
args.emplace_back(std::format(L"--process-handle={}", reinterpret_cast(hInheritableCurrentProcess)));
args.emplace_back(std::format(L"--exception-info-pipe-read-handle={}", reinterpret_cast(hReadPipeInheritable->get())));
args.emplace_back(std::format(L"--asset-directory={}", unicode::convert(g_startInfo.AssetDirectory)));
- args.emplace_back(std::format(L"--log-directory={}", g_startInfo.BootLogPath.empty()
- ? utils::loaded_module(g_hModule).path().parent_path().wstring()
- : std::filesystem::path(unicode::convert(g_startInfo.BootLogPath)).parent_path().wstring()));
+ if (const auto path = utils::loaded_module(g_hModule).path()) {
+ args.emplace_back(std::format(L"--log-directory={}", g_startInfo.BootLogPath.empty()
+ ? path->parent_path().wstring()
+ : std::filesystem::path(unicode::convert(g_startInfo.BootLogPath)).parent_path().wstring()));
+ } else {
+ logging::W("Failed to read path of the Dalamud Boot module: {}", path.error().describe());
+ return false;
+ }
+
args.emplace_back(L"--");
- append_injector_launch_args(args);
+ if (auto r = append_injector_launch_args(args); !r) {
+ logging::W("Failed to generate injector launch args: {}", r.error().describe());
+ return false;
+ }
for (const auto& arg : args)
{
diff --git a/Dalamud.Boot/xivfixes.cpp b/Dalamud.Boot/xivfixes.cpp
index eb0f7df56..7f9e92225 100644
--- a/Dalamud.Boot/xivfixes.cpp
+++ b/Dalamud.Boot/xivfixes.cpp
@@ -8,12 +8,6 @@
#include "ntdll.h"
#include "utils.h"
-template
-static std::span assume_nonempty_span(std::span t, const char* descr) {
- if (t.empty())
- throw std::runtime_error(std::format("Unexpected empty span found: {}", descr));
- return t;
-}
void xivfixes::unhook_dll(bool bApply) {
static const auto LogTag = "[xivfixes:unhook_dll]";
static const auto LogTagW = L"[xivfixes:unhook_dll]";
@@ -23,77 +17,90 @@ void xivfixes::unhook_dll(bool bApply) {
const auto mods = utils::loaded_module::all_modules();
- const auto test_module = [&](size_t i, const utils::loaded_module & mod) {
- std::filesystem::path path;
- try {
- path = mod.path();
- std::wstring version, description;
- try {
- version = utils::format_file_version(mod.get_file_version());
- } catch (...) {
- version = L"";
- }
-
- try {
- description = mod.get_description();
- } catch (...) {
- description = L"";
- }
-
- logging::I(R"({} [{}/{}] Module 0x{:X} ~ 0x{:X} (0x{:X}): "{}" ("{}" ver {}))", LogTagW, i + 1, mods.size(), mod.address_int(), mod.address_int() + mod.image_size(), mod.image_size(), path.wstring(), description, version);
- } catch (const std::exception& e) {
- logging::W("{} [{}/{}] Module 0x{:X}: Failed to resolve path: {}", LogTag, i + 1, mods.size(), mod.address_int(), e.what());
+ for (size_t i = 0; i < mods.size(); i++) {
+ const auto& mod = mods[i];
+ const auto path = mod.path();
+ if (!path) {
+ logging::W(
+ "{} [{}/{}] Module 0x{:X}: Failed to resolve path: {}",
+ LogTag,
+ i + 1,
+ mods.size(),
+ mod.address_int(),
+ path.error().describe());
return;
}
- const auto moduleName = unicode::convert(path.filename().wstring());
+ const auto version = mod.get_file_version()
+ .transform([](const auto& v) { return utils::format_file_version(v.get()); })
+ .value_or(L"");
- std::vector buf;
- std::string formatBuf;
+ const auto description = mod.get_description()
+ .value_or(L"");
+
+ logging::I(
+ R"({} [{}/{}] Module 0x{:X} ~ 0x{:X} (0x{:X}): "{}" ("{}" ver {}))",
+ LogTagW,
+ i + 1,
+ mods.size(),
+ mod.address_int(),
+ mod.address_int() + mod.image_size(),
+ mod.image_size(),
+ path->wstring(),
+ description,
+ version);
+
+ const auto moduleName = unicode::convert(path->filename().wstring());
+
+ const auto& sectionHeader = mod.section_header(".text");
+ const auto section = mod.span_as(sectionHeader.VirtualAddress, sectionHeader.Misc.VirtualSize);
+ if (section.empty()) {
+ logging::W("{} Error: .text[VA:VA + VS] is empty", LogTag);
+ return;
+ }
+
+ auto hFsDllRaw = CreateFileW(path->c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
+ if (hFsDllRaw == INVALID_HANDLE_VALUE) {
+ logging::W("{} Module loaded in current process but could not open file: Win32 error {}", LogTag, GetLastError());
+ return;
+ }
+
+ auto hFsDll = std::unique_ptr(hFsDllRaw, &CloseHandle);
+ std::vector buf(section.size());
+ SetFilePointer(hFsDll.get(), sectionHeader.PointerToRawData, nullptr, FILE_CURRENT);
+ if (DWORD read{}; ReadFile(hFsDll.get(), &buf[0], static_cast(buf.size()), &read, nullptr)) {
+ if (read < section.size_bytes()) {
+ logging::W("{} ReadFile: read {} bytes < requested {} bytes", LogTagW, read, section.size_bytes());
+ return;
+ }
+ } else {
+ logging::I("{} ReadFile: Win32 error {}", LogTagW, GetLastError());
+ return;
+ }
+
+ const auto doRestore = g_startInfo.BootUnhookDlls.contains(unicode::convert(path->filename().u8string()));
try {
- const auto& sectionHeader = mod.section_header(".text");
- const auto section = assume_nonempty_span(mod.span_as(sectionHeader.VirtualAddress, sectionHeader.Misc.VirtualSize), ".text[VA:VA+VS]");
- auto hFsDllRaw = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
- if (hFsDllRaw == INVALID_HANDLE_VALUE) {
- logging::W("{} Module loaded in current process but could not open file: Win32 error {}", LogTag, GetLastError());
- return;
- }
- auto hFsDll = std::unique_ptr(hFsDllRaw, &CloseHandle);
-
- buf.resize(section.size());
- SetFilePointer(hFsDll.get(), sectionHeader.PointerToRawData, nullptr, FILE_CURRENT);
- if (DWORD read{}; ReadFile(hFsDll.get(), &buf[0], static_cast(buf.size()), &read, nullptr)) {
- if (read < section.size_bytes()) {
- logging::W("{} ReadFile: read {} bytes < requested {} bytes", LogTagW, read, section.size_bytes());
- return;
- }
- } else {
- logging::I("{} ReadFile: Win32 error {}", LogTagW, GetLastError());
- return;
- }
-
- const auto doRestore = g_startInfo.BootUnhookDlls.contains(unicode::convert(path.filename().u8string()));
-
std::optional tenderizer;
- for (size_t i = 0, instructionLength = 1, printed = 0; i < buf.size(); i += instructionLength) {
- if (section[i] == buf[i]) {
+ std::string formatBuf;
+ for (size_t inst = 0, instructionLength = 1, printed = 0; inst < buf.size(); inst += instructionLength) {
+ if (section[inst] == buf[inst]) {
instructionLength = 1;
continue;
}
- const auto rva = sectionHeader.VirtualAddress + i;
+ const auto rva = sectionHeader.VirtualAddress + inst;
nmd_x86_instruction instruction{};
- if (!nmd_x86_decode(§ion[i], section.size() - i, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
+ if (!nmd_x86_decode(§ion[inst], section.size() - inst, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
instructionLength = 1;
if (printed < 64) {
- logging::W("{} {}+0x{:0X}: dd {:02X}", LogTag, moduleName, rva, static_cast(section[i]));
+ logging::W("{} {}+0x{:0X}: dd {:02X}", LogTag, moduleName, rva, static_cast(section[inst]));
printed++;
}
} else {
instructionLength = instruction.length;
if (printed < 64) {
formatBuf.resize(128);
- nmd_x86_format(&instruction, &formatBuf[0], reinterpret_cast(§ion[i]), NMD_X86_FORMAT_FLAGS_DEFAULT | NMD_X86_FORMAT_FLAGS_BYTES);
+ nmd_x86_format(&instruction, &formatBuf[0], reinterpret_cast(§ion[inst]), NMD_X86_FORMAT_FLAGS_DEFAULT | NMD_X86_FORMAT_FLAGS_BYTES);
formatBuf.resize(strnlen(&formatBuf[0], formatBuf.size()));
const auto& directory = mod.data_directory(IMAGE_DIRECTORY_ENTRY_EXPORT);
@@ -103,25 +110,25 @@ void xivfixes::unhook_dll(bool bApply) {
const auto functions = mod.span_as(exportDirectory.AddressOfFunctions, exportDirectory.NumberOfFunctions);
std::string resolvedExportName;
- for (size_t j = 0; j < names.size(); ++j) {
+ for (size_t nameIndex = 0; nameIndex < names.size(); ++nameIndex) {
std::string_view name;
- if (const char* pcszName = mod.address_as(names[j]); pcszName < mod.address() || pcszName >= mod.address() + mod.image_size()) {
+ if (const char* pcszName = mod.address_as(names[nameIndex]); pcszName < mod.address() || pcszName >= mod.address() + mod.image_size()) {
if (IsBadReadPtr(pcszName, 256)) {
- logging::W("{} Name #{} points to an invalid address outside the executable. Skipping.", LogTag, j);
+ logging::W("{} Name #{} points to an invalid address outside the executable. Skipping.", LogTag, nameIndex);
continue;
}
name = std::string_view(pcszName, strnlen(pcszName, 256));
- logging::W("{} Name #{} points to a seemingly valid address outside the executable: {}", LogTag, j, name);
+ logging::W("{} Name #{} points to a seemingly valid address outside the executable: {}", LogTag, nameIndex, name);
}
- if (ordinals[j] >= functions.size()) {
- logging::W("{} Ordinal #{} points to function index #{} >= #{}. Skipping.", LogTag, j, ordinals[j], functions.size());
+ if (ordinals[nameIndex] >= functions.size()) {
+ logging::W("{} Ordinal #{} points to function index #{} >= #{}. Skipping.", LogTag, nameIndex, ordinals[nameIndex], functions.size());
continue;
}
- const auto rva = functions[ordinals[j]];
- if (rva == §ion[i] - mod.address()) {
+ const auto rva = functions[ordinals[nameIndex]];
+ if (rva == §ion[inst] - mod.address()) {
resolvedExportName = std::format("[export:{}]", name);
break;
}
@@ -135,7 +142,7 @@ void xivfixes::unhook_dll(bool bApply) {
if (doRestore) {
if (!tenderizer)
tenderizer.emplace(section, PAGE_EXECUTE_READWRITE);
- memcpy(§ion[i], &buf[i], instructionLength);
+ memcpy(§ion[inst], &buf[inst], instructionLength);
}
}
@@ -147,21 +154,7 @@ void xivfixes::unhook_dll(bool bApply) {
} catch (const std::exception& e) {
logging::W("{} Error: {}", LogTag, e.what());
}
- };
-
- // This is needed since try and __try cannot be used in the same function. Lambdas circumvent the limitation.
- const auto windows_exception_handler = [&]() {
- for (size_t i = 0; i < mods.size(); i++) {
- const auto& mod = mods[i];
- __try {
- test_module(i, mod);
- } __except (EXCEPTION_EXECUTE_HANDLER) {
- logging::W("{} Error: Access Violation", LogTag);
- }
- }
- };
-
- windows_exception_handler();
+ }
}
using TFnGetInputDeviceManager = void* ();
@@ -294,13 +287,11 @@ static bool is_xivalex(const std::filesystem::path& dllPath) {
static bool is_openprocess_already_dealt_with() {
static const auto s_value = [] {
for (const auto& mod : utils::loaded_module::all_modules()) {
- try {
- if (is_xivalex(mod.path()))
- return true;
-
- } catch (...) {
- // pass
- }
+ const auto path = mod.path().value_or({});
+ if (path.empty())
+ continue;
+ if (is_xivalex(path))
+ return true;
}
return false;
}();
@@ -650,43 +641,22 @@ void xivfixes::symbol_load_patches(bool bApply) {
void xivfixes::disable_game_debugging_protection(bool bApply) {
static const char* LogTag = "[xivfixes:disable_game_debugging_protection]";
- static const std::vector patchBytes = {
- 0x31, 0xC0, // XOR EAX, EAX
- 0x90, // NOP
- 0x90, // NOP
- 0x90, // NOP
- 0x90 // NOP
- };
+ static std::optional> s_hookIsDebuggerPresent;
- if (!bApply)
- return;
+ if (bApply) {
+ if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) {
+ logging::I("{} Turned off via environment variable.", LogTag);
+ return;
+ }
- if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) {
- logging::I("{} Turned off via environment variable.", LogTag);
- return;
- }
-
- // Find IsDebuggerPresent in Framework.Tick()
- const char* matchPtr = utils::signature_finder()
- .look_in(utils::loaded_module(g_hGameInstance), ".text")
- .look_for_hex("FF 15 ?? ?? ?? ?? 85 C0 74 13 41")
- .find_one()
- .Match.data();
-
- if (!matchPtr) {
- logging::E("{} Failed to find signature.", LogTag);
- return;
- }
-
- void* address = const_cast(static_cast(matchPtr));
-
- DWORD oldProtect;
- if (VirtualProtect(address, patchBytes.size(), PAGE_EXECUTE_READWRITE, &oldProtect)) {
- memcpy(address, patchBytes.data(), patchBytes.size());
- VirtualProtect(address, patchBytes.size(), oldProtect, &oldProtect);
- logging::I("{} Patch applied at address 0x{:X}.", LogTag, reinterpret_cast(address));
+ s_hookIsDebuggerPresent.emplace("kernel32.dll!IsDebuggerPresent", "kernel32.dll", "IsDebuggerPresent", 0);
+ s_hookIsDebuggerPresent->set_detour([]() { return false; });
+ logging::I("{} Enable", LogTag);
} else {
- logging::E("{} Failed to change memory protection.", LogTag);
+ if (s_hookIsDebuggerPresent) {
+ logging::I("{} Disable", LogTag);
+ s_hookIsDebuggerPresent.reset();
+ }
}
}
diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
index 1c89f4ff7..7f8de3843 100644
--- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
+++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
@@ -38,7 +38,7 @@
Level3
true
true
- stdcpplatest
+ stdcpp23
pch.h
ProgramDatabase
CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
@@ -55,7 +55,7 @@
true
false
- MultiThreadedDebugDLL
+ MultiThreadedDebugDLL
_DEBUG;%(PreprocessorDefinitions)
@@ -67,7 +67,7 @@
true
true
- MultiThreadedDLL
+ MultiThreadedDLL
NDEBUG;%(PreprocessorDefinitions)
@@ -108,4 +108,4 @@
-
+
\ No newline at end of file
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index e94501d92..d68bc8bef 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -635,29 +635,6 @@ internal partial class InterfaceManager : IInternalDisposableService
Service.ProvideException(ex);
Log.Error(ex, "Could not load ImGui dependencies.");
- fixed (void* lpText =
- "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?")
- {
- fixed (void* lpCaption = "Dalamud Error")
- {
- var res = MessageBoxW(
- default,
- (ushort*)lpText,
- (ushort*)lpCaption,
- MB.MB_YESNO | MB.MB_TOPMOST | MB.MB_ICONERROR);
-
- if (res == IDYES)
- {
- var psi = new ProcessStartInfo
- {
- FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe",
- UseShellExecute = true,
- };
- Process.Start(psi);
- }
- }
- }
-
Environment.Exit(-1);
// Doesn't reach here, but to make the compiler not complain
From ef688c09e28c22ef920982210347f9a15c8a358a Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Fri, 15 Aug 2025 08:38:04 -0700
Subject: [PATCH 013/477] Now with more child labor (#2374)
---
.../Windows/PluginInstaller/PluginInstallerWindow.cs | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index 3536c9fe7..7bdda58bf 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -2076,10 +2076,15 @@ internal class PluginInstallerWindow : Window, IDisposable
var isOpen = this.openPluginCollapsibles.Contains(index);
var sectionSize = ImGuiHelpers.GlobalScale * 66;
- var tapeCursor = ImGui.GetCursorPos();
ImGui.Separator();
+ var childId = $"plugin_child_{label}_{plugin?.EffectiveWorkingPluginId}_{manifest.InternalName}";
+ const ImGuiWindowFlags childFlags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
+
+ using var pluginChild = ImRaii.Child(childId, new Vector2(ImGui.GetContentRegionAvail().X, sectionSize), false, childFlags);
+ if (!pluginChild) return false;
+
var startCursor = ImGui.GetCursorPos();
if (flags.HasFlag(PluginHeaderFlags.IsTesting))
@@ -2115,7 +2120,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
- DrawCautionTape(tapeCursor + new Vector2(0, 1), new Vector2(ImGui.GetWindowWidth(), sectionSize + ImGui.GetStyle().ItemSpacing.Y), ImGuiHelpers.GlobalScale * 40, 20);
+ DrawCautionTape(startCursor + new Vector2(0, 1), new Vector2(ImGui.GetWindowWidth(), sectionSize + ImGui.GetStyle().ItemSpacing.Y), ImGuiHelpers.GlobalScale * 40, 20);
}
ImGui.PushStyleColor(ImGuiCol.Button, isOpen ? new Vector4(0.5f, 0.5f, 0.5f, 0.1f) : Vector4.Zero);
@@ -2124,7 +2129,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0.5f, 0.5f, 0.5f, 0.35f));
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0);
- ImGui.SetCursorPos(tapeCursor);
+ ImGui.SetCursorPos(startCursor);
if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetContentRegionAvail().X, sectionSize + ImGui.GetStyle().ItemSpacing.Y)))
{
From e2f3fdd0ff519dd6dfdae33d598822a3ba7505b6 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Fri, 15 Aug 2025 18:11:25 +0200
Subject: [PATCH 014/477] ISeStringEvaluator: Add ReadOnlySpan support
(#2370)
* Add EvaluateMacroString ROS overload
* Add implicit ROS to SeStringParameter cast
---
Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 9 +++++++++
Dalamud/Game/Text/Evaluator/SeStringParameter.cs | 2 ++
Dalamud/Plugin/Services/ISeStringEvaluator.cs | 9 +++++++++
3 files changed, 20 insertions(+)
diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
index 3a504b17b..8b6a2bed8 100644
--- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
+++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
@@ -121,6 +121,15 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return this.Evaluate(ReadOnlySeString.FromMacroString(macroString).AsSpan(), localParameters, language);
}
+ ///
+ public ReadOnlySeString EvaluateMacroString(
+ ReadOnlySpan macroString,
+ Span localParameters = default,
+ ClientLanguage? language = null)
+ {
+ return this.Evaluate(ReadOnlySeString.FromMacroString(macroString).AsSpan(), localParameters, language);
+ }
+
///
public ReadOnlySeString EvaluateFromAddon(
uint addonId,
diff --git a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
index 7201179ea..1c6dd96cb 100644
--- a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
+++ b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
@@ -77,4 +77,6 @@ public readonly struct SeStringParameter
public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode()));
public static implicit operator SeStringParameter(string value) => new(value);
+
+ public static implicit operator SeStringParameter(ReadOnlySpan value) => new(value);
}
diff --git a/Dalamud/Plugin/Services/ISeStringEvaluator.cs b/Dalamud/Plugin/Services/ISeStringEvaluator.cs
index 65932652e..4efc29e3e 100644
--- a/Dalamud/Plugin/Services/ISeStringEvaluator.cs
+++ b/Dalamud/Plugin/Services/ISeStringEvaluator.cs
@@ -38,6 +38,15 @@ public interface ISeStringEvaluator
/// An evaluated .
ReadOnlySeString EvaluateMacroString(string macroString, Span localParameters = default, ClientLanguage? language = null);
+ ///
+ /// Evaluates macros in a macro string.
+ ///
+ /// The macro string.
+ /// An optional list of local parameters.
+ /// An optional language override.
+ /// An evaluated .
+ ReadOnlySeString EvaluateMacroString(ReadOnlySpan macroString, Span localParameters = default, ClientLanguage? language = null);
+
///
/// Evaluates macros in text from the Addon sheet.
///
From f687852879f5accc14f1c7486623d3bbaf1187a1 Mon Sep 17 00:00:00 2001
From: Haselnussbomber
Date: Fri, 15 Aug 2025 18:19:21 +0200
Subject: [PATCH 015/477] Set ClientState.TerritoryType on load (#2366)
* Set TerritoryType on load
* Do not fire TerritoryChanged event on load
---
Dalamud/Game/ClientState/ClientState.cs | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs
index 4c32873c6..13b75bda0 100644
--- a/Dalamud/Game/ClientState/ClientState.cs
+++ b/Dalamud/Game/ClientState/ClientState.cs
@@ -72,6 +72,8 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.setupTerritoryTypeHook.Enable();
this.uiModuleHandlePacketHook.Enable();
this.onLogoutHook.Enable();
+
+ this.framework.RunOnTick(this.Setup);
}
private unsafe delegate void ProcessPacketPlayerSetupDelegate(nint a1, nint packet);
@@ -180,8 +182,22 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
}
+ private unsafe void Setup()
+ {
+ this.TerritoryType = (ushort)GameMain.Instance()->CurrentTerritoryTypeId;
+ }
+
private unsafe void SetupTerritoryTypeDetour(EventFramework* eventFramework, ushort territoryType)
{
+ this.SetTerritoryType(territoryType);
+ this.setupTerritoryTypeHook.Original(eventFramework, territoryType);
+ }
+
+ private unsafe void SetTerritoryType(ushort territoryType)
+ {
+ if (this.TerritoryType == territoryType)
+ return;
+
Log.Debug("TerritoryType changed: {0}", territoryType);
this.TerritoryType = territoryType;
@@ -207,8 +223,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
}
}
}
-
- this.setupTerritoryTypeHook.Original(eventFramework, territoryType);
}
private unsafe void UIModuleHandlePacketDetour(
From e684e7e208748e9c8a90c74a34bca175c3ae987c Mon Sep 17 00:00:00 2001
From: bleatbot <106497096+bleatbot@users.noreply.github.com>
Date: Fri, 15 Aug 2025 18:20:38 +0200
Subject: [PATCH 016/477] Update ClientStructs (#2371)
Co-authored-by: github-actions[bot]
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index af8bb59e7..59930d493 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit af8bb59e79a4f50191dc7a5fc67e86a2624c934f
+Subproject commit 59930d4934585743e6d5f4f3ce1a3001fdf3f0f6
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 017/477] 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 2affbe36838aa93523a3bd7b4f1c9d59e729dab1 Mon Sep 17 00:00:00 2001
From: goaaats
Date: Sun, 17 Aug 2025 13:10:21 +0200
Subject: [PATCH 018/477] build: 13.0.0.2
---
Dalamud/Dalamud.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 4ecee7bbf..8049eee77 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -6,7 +6,7 @@
XIV Launcher addon framework
- 13.0.0.1
+ 13.0.0.2
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
From 32cb6e21278ccfbab72704cfd77a7519e2854b3d Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Tue, 19 Aug 2025 11:07:14 -0700
Subject: [PATCH 019/477] feat: Identify the plugin causing an assertion
failure
---
.../Internal/Asserts/AssertHandler.cs | 60 +++++++++++++------
1 file changed, 41 insertions(+), 19 deletions(-)
diff --git a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs
index 91323f8ac..d596956c6 100644
--- a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs
+++ b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs
@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
+using Dalamud.Plugin.Internal;
using Dalamud.Utility;
using Serilog;
@@ -55,7 +56,8 @@ internal class AssertHandler : IDisposable
///
public unsafe void Setup()
{
- CustomNativeFunctions.igCustom_SetAssertCallback(Marshal.GetFunctionPointerForDelegate(this.callback).ToPointer());
+ CustomNativeFunctions.igCustom_SetAssertCallback(
+ Marshal.GetFunctionPointerForDelegate(this.callback).ToPointer());
}
///
@@ -78,10 +80,11 @@ internal class AssertHandler : IDisposable
var file = Marshal.PtrToStringAnsi(new IntPtr(pFile));
if (expr == null || file == null)
{
- Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line} (failed to parse)",
- expr,
- file,
- line);
+ Log.Warning(
+ "ImGui assertion failed: {Expr} at {File}:{Line} (failed to parse)",
+ expr,
+ file,
+ line);
return;
}
@@ -93,7 +96,7 @@ internal class AssertHandler : IDisposable
if (!this.ShowAsserts && !this.everShownAssertThisSession)
return;
- Lazy stackTrace = new(() => DiagnosticUtil.GetUsefulTrace(new StackTrace()).ToString());
+ Lazy stackTrace = new(() => DiagnosticUtil.GetUsefulTrace(new StackTrace()));
if (!this.EnableVerboseLogging)
{
@@ -103,11 +106,12 @@ internal class AssertHandler : IDisposable
if (count <= HideThreshold || count % HidePrintEvery == 0)
{
- Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line} (repeated {Count} times)",
- expr,
- file,
- line,
- count);
+ Log.Warning(
+ "ImGui assertion failed: {Expr} at {File}:{Line} (repeated {Count} times)",
+ expr,
+ file,
+ line,
+ count);
}
}
else
@@ -117,11 +121,12 @@ internal class AssertHandler : IDisposable
}
else
{
- Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line}\n{StackTrace:l}",
- expr,
- file,
- line,
- stackTrace.Value);
+ Log.Warning(
+ "ImGui assertion failed: {Expr} at {File}:{Line}\n{StackTrace:l}",
+ expr,
+ file,
+ line,
+ stackTrace.Value.ToString());
}
if (!this.ShowAsserts)
@@ -145,7 +150,7 @@ internal class AssertHandler : IDisposable
}
// grab the stack trace now that we've decided to show UI.
- _ = stackTrace.Value;
+ var responsiblePlugin = Service.GetNullable()?.FindCallingPlugin(stackTrace.Value);
var gitHubUrl = GetRepoUrl();
var showOnGitHubButton = new TaskDialogButton
@@ -175,12 +180,29 @@ internal class AssertHandler : IDisposable
var ignoreButton = TaskDialogButton.Ignore;
TaskDialogButton? result = null;
+
void DialogThreadStart()
{
// TODO(goat): This is probably not gonna work if we showed the loading dialog
// this session since it already loaded visual styles...
Application.EnableVisualStyles();
+ string text;
+ if (responsiblePlugin != null)
+ {
+ text = $"The plugin \"{responsiblePlugin.Name}\" appears to have caused an ImGui assertion failure. " +
+ $"Please report this problem to the plugin's developer.\n\n";
+ }
+ else
+ {
+ text = "Some code in a plugin or Dalamud itself has caused an ImGui assertion failure. " +
+ "Please report this problem in the Dalamud discord.\n\n";
+ }
+
+ text += $"You may attempt to continue running the game, but Dalamud UI elements may not work " +
+ $"correctly, or the game may crash after resuming.\n\n" +
+ $"{expr}\nAt: {file}:{line}";
+
var page = new TaskDialogPage
{
Heading = "ImGui assertion failed",
@@ -189,9 +211,9 @@ internal class AssertHandler : IDisposable
{
CollapsedButtonText = "Show stack trace",
ExpandedButtonText = "Hide stack trace",
- Text = stackTrace.Value,
+ Text = stackTrace.Value.ToString(),
},
- Text = $"Some code in a plugin or Dalamud itself has caused an internal assertion in ImGui to fail. The game will most likely crash now.\n\n{expr}\nAt: {file}:{line}",
+ Text = text,
Icon = TaskDialogIcon.Warning,
Buttons =
[
From 9e405b26d232e5b82a799f19e510a94970cf5e5f Mon Sep 17 00:00:00 2001
From: Kaz Wolfe
Date: Tue, 19 Aug 2025 12:16:34 -0700
Subject: [PATCH 020/477] feat: include line numbers/file info in stacktrace
---
Dalamud/Interface/Internal/Asserts/AssertHandler.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs
index d596956c6..d9f48ab54 100644
--- a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs
+++ b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs
@@ -96,7 +96,7 @@ internal class AssertHandler : IDisposable
if (!this.ShowAsserts && !this.everShownAssertThisSession)
return;
- Lazy