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 stackTrace = new(() => DiagnosticUtil.GetUsefulTrace(new StackTrace())); + Lazy stackTrace = new(() => DiagnosticUtil.GetUsefulTrace(new StackTrace(true))); if (!this.EnableVerboseLogging) { From 0c9176a8b6190ee2e86accb7db9d925b49e64857 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 19 Aug 2025 12:44:34 -0700 Subject: [PATCH 021/477] feat: Reword message overview - Removes extra lines from stack trace - Use clearer-ish wording for messaging --- .../Internal/Asserts/AssertHandler.cs | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs index d9f48ab54..a905ec132 100644 --- a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs +++ b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; @@ -74,6 +75,42 @@ internal class AssertHandler : IDisposable this.Shutdown(); } + private static string? ExtractImguiFunction(StackTrace stackTrace) + { + var frame = stackTrace.GetFrames() + .FirstOrDefault(f => f.GetMethod()?.DeclaringType?.Namespace == "Dalamud.Bindings.ImGui"); + if (frame == null) + return null; + + var method = frame.GetMethod(); + if (method == null) + return null; + + return $"{method.Name}({string.Join(", ", method.GetParameters().Select(p => p.Name))})"; + } + + private static StackTrace GenerateStackTrace() + { + var trace = DiagnosticUtil.GetUsefulTrace(new StackTrace(true)); + var frames = trace.GetFrames().ToList(); + + // Remove everything that happens in the assert context. + var lastAssertIdx = frames.FindLastIndex(f => f.GetMethod()?.DeclaringType == typeof(AssertHandler)); + if (lastAssertIdx >= 0) + { + frames.RemoveRange(0, lastAssertIdx + 1); + } + + var firstInterfaceManagerIdx = frames.FindIndex(f => f.GetMethod()?.DeclaringType == typeof(InterfaceManager)); + if (firstInterfaceManagerIdx >= 0) + { + frames.RemoveRange(firstInterfaceManagerIdx, frames.Count - firstInterfaceManagerIdx); + } + + return new StackTrace(frames); + } + + private unsafe void OnImGuiAssert(void* pExpr, void* pFile, int line) { var expr = Marshal.PtrToStringAnsi(new IntPtr(pExpr)); @@ -96,7 +133,7 @@ internal class AssertHandler : IDisposable if (!this.ShowAsserts && !this.everShownAssertThisSession) return; - Lazy stackTrace = new(() => DiagnosticUtil.GetUsefulTrace(new StackTrace(true))); + Lazy stackTrace = new(GenerateStackTrace); if (!this.EnableVerboseLogging) { @@ -151,6 +188,7 @@ internal class AssertHandler : IDisposable // grab the stack trace now that we've decided to show UI. var responsiblePlugin = Service.GetNullable()?.FindCallingPlugin(stackTrace.Value); + var responsibleMethodCall = ExtractImguiFunction(stackTrace.Value); var gitHubUrl = GetRepoUrl(); var showOnGitHubButton = new TaskDialogButton @@ -200,8 +238,16 @@ internal class AssertHandler : IDisposable } 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}"; + $"correctly, or the game may crash after resuming.\n\n"; + + if (responsibleMethodCall != null) + { + text += $"Assertion failed: {expr} when performing {responsibleMethodCall}\n{file}:{line}"; + } + else + { + text += $"Assertion failed: {expr}\nAt: {file}:{line}"; + } var page = new TaskDialogPage { From 4a743e9060e22adef0c593d77e332281e8b82468 Mon Sep 17 00:00:00 2001 From: R Date: Tue, 19 Aug 2025 14:35:55 -0700 Subject: [PATCH 022/477] Collect additional logs for crash handler (#2376) * Collect launcher.log on crashes This has been missing for Crash Handler based tspacks for a while, which makes it harder to get information from XLCore users because of our differing log names. (Really, we should rename both to just be xivlauncher.log or something, but that would break existing tooling) * Collect wine.log on crashes --- DalamudCrashHandler/DalamudCrashHandler.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 62ccdd20a..ec7115ffd 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -463,12 +463,14 @@ void open_folder_and_select_items(HWND hwndOpener, const std::wstring& path) { void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const std::string& crashLog, const std::string& troubleshootingPackData) { static const char* SourceLogFiles[] = { - "output.log", + "output.log", // XIVLauncher for Windows + "launcher.log", // XIVLauncher.Core for [mostly] Linux "patcher.log", "dalamud.log", "dalamud.injector.log", "dalamud.boot.log", "aria.log", + "wine.log" }; static constexpr auto MaxSizePerLog = 1 * 1024 * 1024; static constexpr std::array OutputFileTypeFilterSpec{{ From 1702c3be2978d073c97b6d1783de9c320a980e1e Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:44:26 +0200 Subject: [PATCH 023/477] Update ClientStructs (#2375) 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 59930d493..79a409e23 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 59930d4934585743e6d5f4f3ce1a3001fdf3f0f6 +Subproject commit 79a409e234071294ff82f77cf7386de8430c639c From ba9720cb65e8eea961a9bf39aadd595a4d1fbd84 Mon Sep 17 00:00:00 2001 From: Lyna <19539165+Blooym@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:06:52 +0200 Subject: [PATCH 024/477] Add Platform/OS to plugin feedback (#2379) --- Dalamud/Support/BugBait.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index 8dbf2e429..7ce96208c 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -36,6 +36,7 @@ internal static class BugBait Reporter = reporter, Name = plugin.InternalName, Version = isTesting ? plugin.TestingAssemblyVersion?.ToString() : plugin.AssemblyVersion.ToString(), + Platform = Util.GetHostPlatform().ToString(), DalamudHash = Util.GetScmVersion(), }; @@ -66,6 +67,9 @@ internal static class BugBait [JsonProperty("version")] public string? Version { get; set; } + [JsonProperty("platform")] + public string? Platform { get; set; } + [JsonProperty("reporter")] public string? Reporter { get; set; } From 005699e47226f9c899ba2f6c40b010f09207b4b9 Mon Sep 17 00:00:00 2001 From: Blair Date: Fri, 22 Aug 2025 23:03:44 +1000 Subject: [PATCH 025/477] Allow versionless bans (#2381) --- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- Dalamud/Plugin/Internal/Types/BannedPlugin.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index a4aa3919b..db803caa8 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1240,7 +1240,7 @@ internal class PluginManager : IInternalDisposableService } return this.bannedPlugins.Any(ban => (ban.Name == manifest.InternalName || ban.Name == Hash.GetStringSha256Hash(manifest.InternalName)) - && ban.AssemblyVersion >= versionToCheck); + && (ban.AssemblyVersion == null || ban.AssemblyVersion >= versionToCheck)); } /// diff --git a/Dalamud/Plugin/Internal/Types/BannedPlugin.cs b/Dalamud/Plugin/Internal/Types/BannedPlugin.cs index a21bbf02b..384318a56 100644 --- a/Dalamud/Plugin/Internal/Types/BannedPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/BannedPlugin.cs @@ -17,7 +17,7 @@ internal struct BannedPlugin /// Gets plugin assembly version. /// [JsonProperty] - public Version AssemblyVersion { get; private set; } + public Version? AssemblyVersion { get; private set; } /// /// Gets reason for the ban. From b18b8b40e5b5505bc85c4895af2aa20e922e1ef8 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 25 Aug 2025 13:14:13 -0700 Subject: [PATCH 026/477] feat: Add IPC Context - Demo of the ipc context idea - Accessible via ipcPub.GetContext() --- .../Windows/Data/Widgets/PluginIpcWidget.cs | 36 +++++++---- Dalamud/Plugin/DalamudPluginInterface.cs | 36 +++++------ Dalamud/Plugin/Ipc/ICallGateProvider.cs | 3 + .../Plugin/Ipc/Internal/CallGateChannel.cs | 1 + Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs | 56 +++++++++--------- .../Plugin/Ipc/Internal/CallGatePubSubBase.cs | 59 +++++++++++++++++-- Dalamud/Plugin/Ipc/IpcContext.cs | 15 +++++ 7 files changed, 143 insertions(+), 63 deletions(-) create mode 100644 Dalamud/Plugin/Ipc/IpcContext.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs index 6c581604e..446a5e7a9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs @@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Internal; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -48,12 +49,20 @@ internal class PluginIpcWidget : IDataWindowWidget this.ipcPub.RegisterAction(msg => { - Log.Information("Data action was called: {Msg}", msg); + Log.Information( + "Data action was called: {Msg}\n" + + " Context: {Context}", + msg, + this.ipcPub.GetContext()); }); this.ipcPub.RegisterFunc(msg => { - Log.Information("Data func was called: {Msg}", msg); + Log.Information( + "Data func was called: {Msg}\n" + + " Context: {Context}", + msg, + this.ipcPub.GetContext()); return Guid.NewGuid().ToString(); }); } @@ -61,14 +70,8 @@ internal class PluginIpcWidget : IDataWindowWidget if (this.ipcSub == null) { this.ipcSub = new CallGatePubSub("dataDemo1"); - this.ipcSub.Subscribe(_ => - { - Log.Information("PONG1"); - }); - this.ipcSub.Subscribe(_ => - { - Log.Information("PONG2"); - }); + this.ipcSub.Subscribe(_ => { Log.Information("PONG1"); }); + this.ipcSub.Subscribe(_ => { Log.Information("PONG2"); }); this.ipcSub.Subscribe(_ => throw new Exception("PONG3")); } @@ -78,12 +81,21 @@ internal class PluginIpcWidget : IDataWindowWidget this.ipcPubGo.RegisterAction(go => { - Log.Information("Data action was called: {Name}", go?.Name); + Log.Information( + "Data action was called: {Name}" + + "\n Context: {Context}", + go?.Name, + this.ipcPubGo.GetContext()); }); this.ipcPubGo.RegisterFunc(go => { - Log.Information("Data func was called: {Name}", go?.Name); + Log.Information( + "Data func was called: {Name}\n" + + " Context: {Context}", + go?.Name, + this.ipcPubGo.GetContext()); + return "test"; }); } diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 541071b63..db9320079 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -293,39 +293,39 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// An IPC provider. /// This is thrown when the requested types do not match the previously registered types are different. public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// /// Gets an IPC subscriber. @@ -334,39 +334,39 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// The name of the IPC registration. /// An IPC subscriber. public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); #endregion diff --git a/Dalamud/Plugin/Ipc/ICallGateProvider.cs b/Dalamud/Plugin/Ipc/ICallGateProvider.cs index f4e5c76d7..387f0adf9 100644 --- a/Dalamud/Plugin/Ipc/ICallGateProvider.cs +++ b/Dalamud/Plugin/Ipc/ICallGateProvider.cs @@ -19,6 +19,9 @@ public interface ICallGateProvider /// public void UnregisterFunc(); + + /// + public IpcContext? GetContext(); } /// diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index ea94103f7..698f0917e 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Linq; using System.Reflection; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal.Converters; diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs index cc54a563b..8725ef733 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Internal.Types; + #pragma warning disable SA1402 // File may only contain a single type namespace Dalamud.Plugin.Ipc.Internal; @@ -5,9 +7,9 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -43,9 +45,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -81,9 +83,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider< /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -119,9 +121,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvi /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -157,9 +159,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateP /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -195,9 +197,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallG /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -233,9 +235,9 @@ internal class CallGatePubSub : CallGatePubSubBase, IC /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -271,9 +273,9 @@ internal class CallGatePubSub : CallGatePubSubBase /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -309,9 +311,9 @@ internal class CallGatePubSub : CallGatePubSub /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs index 308457373..24cb5ca11 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs @@ -1,4 +1,11 @@ +using System.Reactive.Disposables; +using System.Threading; + +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Ipc.Exceptions; +using Dalamud.Utility; + +using Serilog; namespace Dalamud.Plugin.Ipc.Internal; @@ -7,13 +14,18 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal abstract class CallGatePubSubBase { + [ThreadStatic] + private static IpcContext? ipcExecutionContext; + /// /// Initializes a new instance of the class. /// /// The name of the IPC registration. - protected CallGatePubSubBase(string name) + /// The plugin that owns this IPC pubsub. + protected CallGatePubSubBase(string name, LocalPlugin? owningPlugin) { this.Channel = Service.Get().GetOrCreateChannel(name); + this.OwningPlugin = owningPlugin; } /// @@ -21,7 +33,7 @@ internal abstract class CallGatePubSubBase /// s. /// public bool HasAction => this.Channel.Action != null; - + /// /// Gets a value indicating whether this IPC call gate has an associated Function. Only exposed to /// s. @@ -33,12 +45,17 @@ internal abstract class CallGatePubSubBase /// s, and can be used to determine if messages should be sent through the gate. /// public int SubscriptionCount => this.Channel.Subscriptions.Count; - + /// /// Gets the underlying channel implementation. /// protected CallGateChannel Channel { get; init; } - + + /// + /// Gets the plugin that owns this pubsub instance. + /// + protected LocalPlugin? OwningPlugin { get; init; } + /// /// Removes the associated Action from this call gate, effectively disabling RPC calls. /// @@ -53,6 +70,16 @@ internal abstract class CallGatePubSubBase public void UnregisterFunc() => this.Channel.Func = null; + /// + /// Gets the current context for this IPC call. This will only be present when called from within an IPC action + /// or function handler, and will be null otherwise. + /// + /// Returns a potential IPC context. + public IpcContext? GetContext() + { + return ipcExecutionContext; + } + /// /// Registers a for use by other plugins via RPC. This Delegate must satisfy the constraints /// of an type as defined by the interface, meaning they may not return a value and must have @@ -105,7 +132,12 @@ internal abstract class CallGatePubSubBase /// /// private protected void InvokeAction(params object?[]? args) - => this.Channel.InvokeAction(args); + { + using (this.BuildContext()) + { + this.Channel.InvokeAction(args); + } + } /// /// Executes the Function registered for this IPC call gate via . This method is intended @@ -120,7 +152,12 @@ internal abstract class CallGatePubSubBase /// /// private protected TRet InvokeFunc(params object?[]? args) - => this.Channel.InvokeFunc(args); + { + using (this.BuildContext()) + { + return this.Channel.InvokeFunc(args); + } + } /// /// Send the given arguments to all subscribers (through ) of this IPC call gate. This method @@ -132,4 +169,14 @@ internal abstract class CallGatePubSubBase /// Delegate arguments. private protected void SendMessage(params object?[]? args) => this.Channel.SendMessage(args); + + private IDisposable BuildContext() + { + ipcExecutionContext = new IpcContext + { + SourcePlugin = this.OwningPlugin != null ? new ExposedPlugin(this.OwningPlugin) : null, + }; + + return Disposable.Create(() => { ipcExecutionContext = null; }); + } } diff --git a/Dalamud/Plugin/Ipc/IpcContext.cs b/Dalamud/Plugin/Ipc/IpcContext.cs new file mode 100644 index 000000000..25fde6a36 --- /dev/null +++ b/Dalamud/Plugin/Ipc/IpcContext.cs @@ -0,0 +1,15 @@ +namespace Dalamud.Plugin.Ipc; + +/// +/// The context associated for an IPC call. Reads from ThreadLocal. +/// +public class IpcContext +{ + /// + /// Gets the plugin that initiated this IPC call. + /// + public IExposedPlugin? SourcePlugin { get; init; } + + /// + public override string ToString() => $""; +} From 8cced4c1d7bece877a12cf69d0f1448a5b352c01 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 25 Aug 2025 13:31:05 -0700 Subject: [PATCH 027/477] fix: use channel threadlocal instead of a ThreadStatic --- Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs | 18 ++++++++++++++++++ .../Plugin/Ipc/Internal/CallGatePubSubBase.cs | 11 ++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index 698f0917e..e177abab7 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; +using System.Threading; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Ipc.Exceptions; @@ -17,6 +18,8 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal class CallGateChannel { + private readonly ThreadLocal ipcExecutionContext = new(); + /// /// The actual storage. /// @@ -146,6 +149,21 @@ internal class CallGateChannel return (TRet)result; } + internal void SetInvocationContext(IpcContext ipcContext) + { + this.ipcExecutionContext.Value = ipcContext; + } + + internal IpcContext? GetInvocationContext() + { + return this.ipcExecutionContext.IsValueCreated ? this.ipcExecutionContext.Value : null; + } + + internal void ClearInvocationContext() + { + this.ipcExecutionContext.Value = null; + } + private void CheckAndConvertArgs(object?[]? args, MethodInfo methodInfo) { var paramTypes = methodInfo.GetParameters() diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs index 24cb5ca11..521824b7b 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs @@ -14,9 +14,6 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal abstract class CallGatePubSubBase { - [ThreadStatic] - private static IpcContext? ipcExecutionContext; - /// /// Initializes a new instance of the class. /// @@ -77,7 +74,7 @@ internal abstract class CallGatePubSubBase /// Returns a potential IPC context. public IpcContext? GetContext() { - return ipcExecutionContext; + return this.Channel.GetInvocationContext(); } /// @@ -172,11 +169,11 @@ internal abstract class CallGatePubSubBase private IDisposable BuildContext() { - ipcExecutionContext = new IpcContext + this.Channel.SetInvocationContext(new IpcContext { SourcePlugin = this.OwningPlugin != null ? new ExposedPlugin(this.OwningPlugin) : null, - }; + }); - return Disposable.Create(() => { ipcExecutionContext = null; }); + return Disposable.Create(() => { this.Channel.ClearInvocationContext(); }); } } From fcf5c2718658b5fd950d06ad21f3e152da596b7e Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:32:09 +0200 Subject: [PATCH 028/477] Update ClientStructs (#2378) 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 79a409e23..0e5c2ea3d 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 79a409e234071294ff82f77cf7386de8430c639c +Subproject commit 0e5c2ea3d1fc683eba0aabfeeb901a3959dedac1 From 6890e59af598fdba2cf4d6c851773e09f18c9efc Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:43:35 +0200 Subject: [PATCH 029/477] Update ClientStructs (#2387) 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 0e5c2ea3d..e36169940 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 0e5c2ea3d1fc683eba0aabfeeb901a3959dedac1 +Subproject commit e3616994092bfd7543be72b1d159b2ff5cebe832 From 80c2985795e355470fba232dbb62ce8194de549e Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 26 Aug 2025 23:00:59 +0200 Subject: [PATCH 030/477] Update ClientStructs (#2389) 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 e36169940..4fbd01f1b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e3616994092bfd7543be72b1d159b2ff5cebe832 +Subproject commit 4fbd01f1b1c4ac740f2b6e64ad5efcad10d227a0 From e3498f1b9ceb5c9b7aa4d131ca550b99bf735fb2 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 30 Aug 2025 03:59:05 -0700 Subject: [PATCH 031/477] Fix entries yeeting themselves out of existance when scrolled off (#2392) --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 7bdda58bf..946a5f19a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2083,7 +2083,10 @@ internal class PluginInstallerWindow : Window, IDisposable 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; + if (!pluginChild) + { + return isOpen; + } var startCursor = ImGui.GetCursorPos(); From 852b1289e2f8bc1a7027715fd93e0ef49c6c08e5 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:59:20 +0200 Subject: [PATCH 032/477] Update ClientStructs (#2390) 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 4fbd01f1b..834d3cc94 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 4fbd01f1b1c4ac740f2b6e64ad5efcad10d227a0 +Subproject commit 834d3cc94082fd812cf6cda8da217b22e4f9f15d From 7f3fc5aac162696c48522fa2324bec8e2753f513 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 2 Sep 2025 14:01:12 +0200 Subject: [PATCH 033/477] Use AgentLobbys OnLogout vfunc (#2395) --- Dalamud/Game/ClientState/ClientState.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 13b75bda0..407b54060 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -38,7 +38,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private readonly ClientStateAddressResolver address; private readonly Hook setupTerritoryTypeHook; private readonly Hook uiModuleHandlePacketHook; - private readonly Hook onLogoutHook; + private Hook onLogoutHook; [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -64,14 +64,12 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.setupTerritoryTypeHook = Hook.FromAddress(setTerritoryTypeAddr, this.SetupTerritoryTypeDetour); this.uiModuleHandlePacketHook = Hook.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour); - this.onLogoutHook = Hook.FromAddress((nint)LogoutCallbackInterface.StaticVirtualTablePointer->OnLogout, this.OnLogoutDetour); this.framework.Update += this.FrameworkOnOnUpdateEvent; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; this.setupTerritoryTypeHook.Enable(); this.uiModuleHandlePacketHook.Enable(); - this.onLogoutHook.Enable(); this.framework.RunOnTick(this.Setup); } @@ -184,6 +182,9 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private unsafe void Setup() { + this.onLogoutHook = Hook.FromAddress((nint)AgentLobby.Instance()->LogoutCallbackInterface.VirtualTable->OnLogout, this.OnLogoutDetour); + this.onLogoutHook.Enable(); + this.TerritoryType = (ushort)GameMain.Instance()->CurrentTerritoryTypeId; } From 7aa77aa2eb33752bc0d222c62e07e571b30f9eeb Mon Sep 17 00:00:00 2001 From: Infi Date: Tue, 2 Sep 2025 14:06:02 +0200 Subject: [PATCH 034/477] - Use ToMacroString for better understanding what changed (#2393) --- Dalamud/Game/Gui/ChatGui.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 72b8ef8d1..d7303c4ce 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -415,13 +415,13 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui if (!terminatedSender.SequenceEqual(possiblyModifiedSenderData)) { - Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(terminatedSender)} -> {parsedSender}"); + Log.Verbose($"HandlePrintMessageDetour Sender modified: {new ReadOnlySeStringSpan(terminatedSender).ToMacroString()} -> {new ReadOnlySeStringSpan(possiblyModifiedSenderData).ToMacroString()}"); sender->SetString(possiblyModifiedSenderData); } if (!terminatedMessage.SequenceEqual(possiblyModifiedMessageData)) { - Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(terminatedMessage)} -> {parsedMessage}"); + Log.Verbose($"HandlePrintMessageDetour Message modified: {new ReadOnlySeStringSpan(terminatedMessage).ToMacroString()} -> {new ReadOnlySeStringSpan(possiblyModifiedMessageData).ToMacroString()}"); message->SetString(possiblyModifiedMessageData); } From 97196a1e350ab3c8c9924fd6942354747d33d5aa Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:13:17 +0200 Subject: [PATCH 035/477] Update ClientStructs (#2396) 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 834d3cc94..3ae5a65f6 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 834d3cc94082fd812cf6cda8da217b22e4f9f15d +Subproject commit 3ae5a65f6d3a00d2d1091319ddec1442ea22726a From 29c4d4939fc92f30bba5a54a67ae2a9400c78cfa Mon Sep 17 00:00:00 2001 From: Limiana <5073202+Limiana@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:06:06 +0300 Subject: [PATCH 036/477] Fix SigScanner.ScanAllText only returning a single match --- Dalamud/Game/SigScanner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index dc1e54e77..67f4f563e 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -343,11 +343,11 @@ public class SigScanner : IDisposable, ISigScanner break; var scanRet = mBase + index; + mBase = scanRet + 1; if (this.IsCopy) scanRet -= this.moduleCopyOffset; yield return scanRet; - mBase = scanRet + 1; } } From 1b08d986caf238c70628a8889b3eb3b8eada1c66 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 3 Sep 2025 01:51:14 +0200 Subject: [PATCH 037/477] build: 13.0.0.3 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 8049eee77..01eb1a2ec 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.2 + 13.0.0.3 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 0047e24031eaa9455826cdfd69c87f4846795369 Mon Sep 17 00:00:00 2001 From: Caraxi Date: Sat, 6 Sep 2025 14:02:30 +0930 Subject: [PATCH 038/477] fix ViewportTextureWrap (#2402) --- .../Textures/TextureWraps/Internal/ViewportTextureWrap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs index 2247fc781..3e0f31eca 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs @@ -56,7 +56,7 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos get { var t = (nint)this.srv.Get(); - return t == nint.Zero ? Service.Get().Empty4X4.Handle : ImTextureID.Null; + return t == nint.Zero ? Service.Get().Empty4X4.Handle : new ImTextureID(this.srv.Get()); } } From f07b308757ec12490bb65ad25eff2592bc8afd27 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:53:38 -0700 Subject: [PATCH 039/477] Add Generic Helper (#2403) --- Dalamud/Game/Gui/GameGui.cs | 10 +++++++++- Dalamud/Plugin/Services/IGameGui.cs | 11 ++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 415dd44c6..4da7b6857 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -182,6 +182,10 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui return (nint)unitManager->GetAddonByName(name, index); } + /// + public T* GetAddonByName(string name, int index = 1) where T : unmanaged + => (T*)this.GetAddonByName(name, index).Address; + /// public AgentInterfacePtr GetAgentById(int id) { @@ -328,7 +332,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui return retVal; } - private unsafe void SetUiVisibilityDetour(RaptureAtkModule* thisPtr, bool uiVisible) + private void SetUiVisibilityDetour(RaptureAtkModule* thisPtr, bool uiVisible) { this.setUiVisibilityHook.Original(thisPtr, uiVisible); @@ -441,6 +445,10 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui public AtkUnitBasePtr GetAddonByName(string name, int index = 1) => this.gameGuiService.GetAddonByName(name, index); + /// + public unsafe T* GetAddonByName(string name, int index = 1) where T : unmanaged + => (T*)this.gameGuiService.GetAddonByName(name, index).Address; + /// public AgentInterfacePtr GetAgentById(int id) => this.gameGuiService.GetAgentById(id); diff --git a/Dalamud/Plugin/Services/IGameGui.cs b/Dalamud/Plugin/Services/IGameGui.cs index 773ba61b4..e2326328a 100644 --- a/Dalamud/Plugin/Services/IGameGui.cs +++ b/Dalamud/Plugin/Services/IGameGui.cs @@ -36,7 +36,7 @@ public unsafe interface IGameGui /// If > 1.000.000, subtract 1.000.000 and treat it as HQ. /// public ulong HoveredItem { get; set; } - + /// /// Gets the action ID that is current hovered by the player. 0 when no action is hovered. /// @@ -89,6 +89,15 @@ public unsafe interface IGameGui /// A pointer wrapper to the addon. public AtkUnitBasePtr GetAddonByName(string name, int index = 1); + /// + /// Gets the pointer to the Addon with the given name and index. + /// + /// Name of addon to find. + /// Index of addon to find (1-indexed). + /// A pointer wrapper to the addon. + /// Type of addon pointer AtkUnitBase or any derived struct. + public T* GetAddonByName(string name, int index = 1) where T : unmanaged; + /// /// Find the agent associated with an addon, if possible. /// From 5b5fdc0c10d411cc65078d16ce420d68b9d2b965 Mon Sep 17 00:00:00 2001 From: Asriel Date: Wed, 10 Sep 2025 16:55:25 -0700 Subject: [PATCH 040/477] Generalize submodules workflow to add Lumina.Excel (#2340) --- .github/workflows/update-submodules.yml | 29 +++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/workflows/update-submodules.yml b/.github/workflows/update-submodules.yml index 19bd86b30..61a900395 100644 --- a/.github/workflows/update-submodules.yml +++ b/.github/workflows/update-submodules.yml @@ -1,16 +1,23 @@ -name: Check for FFXIVCS changes +name: Check for Submodule Changes on: schedule: - - cron: "0 0,12,18 */1 * *" + - cron: "0 0,12,18 * * *" workflow_dispatch: jobs: check: - name: FFXIVCS Check + name: Check ${{ matrix.submodule.name }} runs-on: ubuntu-latest strategy: matrix: branches: [master] + submodule: + - name: ClientStructs + path: lib/FFXIVClientStructs + branch-prefix: csupdate + - name: Excel Schema + path: lib/Lumina.Excel + branch-prefix: schemaupdate defaults: run: @@ -24,7 +31,7 @@ jobs: ref: ${{ matrix.branches }} token: ${{ secrets.UPDATE_PAT }} - name: Create update branch - run: git checkout -b csupdate/${{ matrix.branches }} + run: git checkout -b ${{ matrix.submodule.branch-prefix }}/${{ matrix.branches }} - name: Initialize mandatory git config run: | git config --global user.name "github-actions[bot]" @@ -32,22 +39,22 @@ jobs: git config --global pull.rebase false - name: Update submodule run: | - git checkout -b csupdate-${{ matrix.branches }} + git checkout -b ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} git reset --hard origin/${{ matrix.branches }} - cd lib/FFXIVClientStructs + cd ${{ matrix.submodule.path }} git fetch git reset --hard origin/main cd ../.. - git add lib/FFXIVClientStructs - git commit --message "Update ClientStructs" - git push origin csupdate-${{ matrix.branches }} --force + git add ${{ matrix.submodule.path }} + git commit --message "Update ${{ matrix.submodule.name }}" + git push origin ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --force - name: Create PR run: | echo ${{ secrets.UPDATE_PAT }} | gh auth login --with-token - prNumber=$(gh pr list --base ${{ matrix.branches }} --head csupdate-${{ matrix.branches }} --state open --json number --template "{{range .}}{{.number}}{{end}}") + prNumber=$(gh pr list --base ${{ matrix.branches }} --head ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --state open --json number --template "{{range .}}{{.number}}{{end}}") if [ -z "$prNumber" ]; then echo "No PR found, creating one" - gh pr create --head csupdate-${{ matrix.branches }} --title "[${{ matrix.branches }}] Update ClientStructs" --body "" --base ${{ matrix.branches }} + gh pr create --head ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --title "[${{ matrix.branches }}] Update ${{ matrix.submodule.name }}" --body "" --base ${{ matrix.branches }} else echo "PR already exists, ignoring" fi From a2b3fb901e35506c00ffb414864e8f2719a41d09 Mon Sep 17 00:00:00 2001 From: Asriel Date: Thu, 11 Sep 2025 10:06:07 -0700 Subject: [PATCH 041/477] Fix Lumina.Excel branch in workflow (#2407) --- .github/workflows/update-submodules.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-submodules.yml b/.github/workflows/update-submodules.yml index 61a900395..686974ad5 100644 --- a/.github/workflows/update-submodules.yml +++ b/.github/workflows/update-submodules.yml @@ -1,7 +1,7 @@ name: Check for Submodule Changes on: schedule: - - cron: "0 0,12,18 * * *" + - cron: "0 0,6,12,18 * * *" workflow_dispatch: jobs: @@ -9,14 +9,17 @@ jobs: name: Check ${{ matrix.submodule.name }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: branches: [master] submodule: - name: ClientStructs path: lib/FFXIVClientStructs + branch: main branch-prefix: csupdate - name: Excel Schema path: lib/Lumina.Excel + branch: master branch-prefix: schemaupdate defaults: @@ -43,7 +46,7 @@ jobs: git reset --hard origin/${{ matrix.branches }} cd ${{ matrix.submodule.path }} git fetch - git reset --hard origin/main + git reset --hard origin/${{ matrix.submodule.branch }} cd ../.. git add ${{ matrix.submodule.path }} git commit --message "Update ${{ matrix.submodule.name }}" From fa58d7b3ccef1caad89c7a51ac2abb3559e781bb Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 12 Sep 2025 01:42:45 +0200 Subject: [PATCH 042/477] build: 13.0.0.4 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 01eb1a2ec..59cdc1393 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.3 + 13.0.0.4 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From bf4fc7864f134e9759f1109b97628a70882927d7 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Sep 2025 00:33:23 +0200 Subject: [PATCH 043/477] Handle multiple col- entries in Completion.LookupTable --- .../Game/Text/Evaluator/SeStringEvaluator.cs | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 8b6a2bed8..cd981ae75 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1629,7 +1629,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator return true; var isNoun = false; - var col = 0; + + Span cols = stackalloc int[2]; + cols.Clear(); if (ranges.StartsWith("noun")) { @@ -1637,11 +1639,24 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator } else if (ranges.StartsWith("col")) { - var colRangeEnd = ranges.IndexOf(','); - if (colRangeEnd == -1) - colRangeEnd = ranges.Length; + var i = 0; + while (i < cols.Length && ranges.StartsWith("col-", StringComparison.Ordinal)) + { + // find the end of the current "col-#" token + var colRangeEnd = ranges.IndexOf(','); + if (colRangeEnd == -1) + colRangeEnd = ranges.Length; - col = int.Parse(ranges[4..colRangeEnd]); + // parse the column index + cols[i++] = int.Parse(ranges.AsSpan(4, colRangeEnd - 4)); + + // if it's the end of the string, we're done + if (colRangeEnd == ranges.Length) + break; + + // move to the next entry + ranges = ranges[(colRangeEnd + 1)..].TrimStart(); + } } else if (ranges.StartsWith("tail")) { @@ -1663,7 +1678,17 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator } else if (this.dataManager.GetExcelSheet(context.Language, sheetName).TryGetRow(rowId, out var row)) { - context.Builder.Append(row.ReadStringColumn(col)); + // the game uses a priority system for columns here. + // if the first column is empty (for example MainCommand.Alias), then the next one is used (MainCommand.Command) + for (var i = 0; i < cols.Length; i++) + { + var text = row.ReadStringColumn(cols[i]); + if (!text.IsEmpty) + { + context.Builder.Append(text); + break; + } + } } return true; From 327ebf3bb3973971f37349202d2b149b62e27594 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Sep 2025 01:54:47 +0200 Subject: [PATCH 044/477] Pass correct length to DecodeImpl --- Dalamud/Game/Text/SeStringHandling/Payload.cs | 2 +- Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs index 7131a88a7..3eb1cf317 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs @@ -208,7 +208,7 @@ public abstract partial class Payload } payload ??= new RawPayload((byte)chunkType); - payload.DecodeImpl(reader, reader.BaseStream.Position + chunkLen - 1); + payload.DecodeImpl(reader, reader.BaseStream.Position + chunkLen); // read through the rest of the packet var readBytes = (uint)(reader.BaseStream.Position - packetStart); diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs index a7e41cbc6..02a7c113e 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs @@ -95,14 +95,12 @@ public class RawPayload : Payload /// protected override byte[] EncodeImpl() { - var chunkLen = this.data.Length + 1; - var bytes = new List() { START_BYTE, this.chunkType, - (byte)chunkLen, }; + bytes.AddRange(MakeInteger((uint)this.data.Length)); // chunkLen bytes.AddRange(this.data); bytes.Add(END_BYTE); @@ -113,6 +111,6 @@ public class RawPayload : Payload /// protected override void DecodeImpl(BinaryReader reader, long endOfStream) { - this.data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position + 1)); + this.data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position)); } } From 93a44842ed6b39be12f8acdd1ca3011a05b2061b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Sep 2025 01:55:06 +0200 Subject: [PATCH 045/477] Skip to payload end instead of reading remaining bytes --- Dalamud/Game/Text/SeStringHandling/Payload.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs index 3eb1cf317..c797c4a91 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs @@ -2,11 +2,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using Dalamud.Data; using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Plugin.Services; -using Newtonsoft.Json; using Serilog; // TODOs: @@ -117,7 +114,7 @@ public abstract partial class Payload var chunkType = (SeStringChunkType)reader.ReadByte(); var chunkLen = GetInteger(reader); - var packetStart = reader.BaseStream.Position; + var expressionsStart = reader.BaseStream.Position; // any unhandled payload types will be turned into a RawPayload with the exact same binary data switch (chunkType) @@ -210,9 +207,8 @@ public abstract partial class Payload payload ??= new RawPayload((byte)chunkType); payload.DecodeImpl(reader, reader.BaseStream.Position + chunkLen); - // read through the rest of the packet - var readBytes = (uint)(reader.BaseStream.Position - packetStart); - reader.ReadBytes((int)(chunkLen - readBytes + 1)); // +1 for the END_BYTE marker + // skip to the end of the payload, in case the specific payload handler didn't read everything + reader.BaseStream.Seek(expressionsStart + chunkLen + 1, SeekOrigin.Begin); // +1 for the END_BYTE marker return payload; } From 3f037e5d2026ba80c47abd9a21d8a106563c2e58 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Sep 2025 02:10:01 +0200 Subject: [PATCH 046/477] Use SeStringEvaluator in AutoTranslatePayload --- .../Payloads/AutoTranslatePayload.cs | 116 +++++------------- 1 file changed, 29 insertions(+), 87 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs index b038deb6f..8035cdd74 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; using System.IO; -using System.Linq; -using Dalamud.Data; +using Dalamud.Game.Text.Evaluator; -using Lumina.Excel.Sheets; +using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; using Newtonsoft.Json; -using Serilog; namespace Dalamud.Game.Text.SeStringHandling.Payloads; @@ -18,6 +15,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads; public class AutoTranslatePayload : Payload, ITextProvider { private string? text; + private ReadOnlySeString payload; /// /// Initializes a new instance of the class. @@ -34,6 +32,14 @@ public class AutoTranslatePayload : Payload, ITextProvider // TODO: friendlier ctor? not sure how to handle that given how weird the tables are this.Group = group; this.Key = key; + + var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get(); + this.payload = ssb.BeginMacro(MacroCode.Fixed) + .AppendUIntExpression(group - 1) + .AppendUIntExpression(key) + .EndMacro() + .ToReadOnlySeString(); + Lumina.Text.SeStringBuilder.SharedPool.Return(ssb); } /// @@ -41,6 +47,7 @@ public class AutoTranslatePayload : Payload, ITextProvider /// internal AutoTranslatePayload() { + this.payload = default; // parsed by DecodeImpl } /// @@ -68,8 +75,13 @@ public class AutoTranslatePayload : Payload, ITextProvider { get { + if (this.Group is 100 or 200) + { + return this.text ??= Service.Get().Evaluate(this.payload).ToString(); + } + // wrap the text in the colored brackets that is uses in-game, since those are not actually part of any of the payloads - return this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {this.Resolve()} {(char)SeIconChar.AutoTranslateClose}"; + return this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {Service.Get().Evaluate(this.payload)} {(char)SeIconChar.AutoTranslateClose}"; } } @@ -85,95 +97,25 @@ public class AutoTranslatePayload : Payload, ITextProvider /// protected override byte[] EncodeImpl() { - var keyBytes = MakeInteger(this.Key); - - var chunkLen = keyBytes.Length + 2; - var bytes = new List() - { - START_BYTE, - (byte)SeStringChunkType.AutoTranslateKey, (byte)chunkLen, - (byte)this.Group, - }; - bytes.AddRange(keyBytes); - bytes.Add(END_BYTE); - - return bytes.ToArray(); + return this.payload.Data.ToArray(); } /// protected override void DecodeImpl(BinaryReader reader, long endOfStream) { - // this seems to always be a bare byte, and not following normal integer encoding - // the values in the table are all <70 so this is presumably ok - this.Group = reader.ReadByte(); + var body = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position)); + var rosps = new ReadOnlySePayloadSpan(ReadOnlySePayloadType.Macro, MacroCode.Fixed, body.AsSpan()); - this.Key = GetInteger(reader); - } + var span = rosps.EnvelopeByteLength <= 512 ? stackalloc byte[rosps.EnvelopeByteLength] : new byte[rosps.EnvelopeByteLength]; + rosps.WriteEnvelopeTo(span); + this.payload = new ReadOnlySeString(span); - private static ReadOnlySeString ResolveTextCommand(TextCommand command) - { - // TextCommands prioritize the `Alias` field, if it not empty - // Example for this is /rangerpose2l which becomes /blackrangerposeb in chat - return !command.Alias.IsEmpty ? command.Alias : command.Command; - } - - private string Resolve() - { - string value = null; - - var excelModule = Service.Get().Excel; - var completionSheet = excelModule.GetSheet(); - - // try to get the row in the Completion table itself, because this is 'easiest' - // The row may not exist at all (if the Key is for another table), or it could be the wrong row - // (again, if it's meant for another table) - - if (completionSheet.GetRowOrDefault(this.Key) is { } completion && completion.Group == this.Group) + if (rosps.TryGetExpression(out var expr1, out var expr2) + && expr1.TryGetUInt(out var group) + && expr2.TryGetUInt(out var key)) { - // if the row exists in this table and the group matches, this is actually the correct data - value = completion.Text.ExtractText(); + this.Group = group; + this.Key = key; } - else - { - try - { - // we need to get the linked table and do the lookup there instead - // in this case, there will only be one entry for this group id - var row = completionSheet.First(r => r.Group == this.Group); - // many of the names contain valid id ranges after the table name, but we don't need those - var actualTableName = row.LookupTable.ExtractText().Split('[')[0]; - - var name = actualTableName switch - { - "Action" => excelModule.GetSheet().GetRow(this.Key).Name, - "ActionComboRoute" => excelModule.GetSheet().GetRow(this.Key).Name, - "BuddyAction" => excelModule.GetSheet().GetRow(this.Key).Name, - "ClassJob" => excelModule.GetSheet().GetRow(this.Key).Name, - "Companion" => excelModule.GetSheet().GetRow(this.Key).Singular, - "CraftAction" => excelModule.GetSheet().GetRow(this.Key).Name, - "GeneralAction" => excelModule.GetSheet().GetRow(this.Key).Name, - "GuardianDeity" => excelModule.GetSheet().GetRow(this.Key).Name, - "MainCommand" => excelModule.GetSheet().GetRow(this.Key).Name, - "Mount" => excelModule.GetSheet().GetRow(this.Key).Singular, - "Pet" => excelModule.GetSheet().GetRow(this.Key).Name, - "PetAction" => excelModule.GetSheet().GetRow(this.Key).Name, - "PetMirage" => excelModule.GetSheet().GetRow(this.Key).Name, - "PlaceName" => excelModule.GetSheet().GetRow(this.Key).Name, - "Race" => excelModule.GetSheet().GetRow(this.Key).Masculine, - "TextCommand" => AutoTranslatePayload.ResolveTextCommand(excelModule.GetSheet().GetRow(this.Key)), - "Tribe" => excelModule.GetSheet().GetRow(this.Key).Masculine, - "Weather" => excelModule.GetSheet().GetRow(this.Key).Name, - _ => throw new Exception(actualTableName), - }; - - value = name.ExtractText(); - } - catch (Exception e) - { - Log.Error(e, $"AutoTranslatePayload - failed to resolve: {this.Type} - Group: {this.Group}, Key: {this.Key}"); - } - } - - return value; } } From d8555f207ed44da310280f4501bdcde24161c236 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Sep 2025 03:53:26 +0200 Subject: [PATCH 047/477] Rework range check - Loops through all entries - Bumped amount of cols up to 8 for future proofing - Use Ordinal search - Actually parse ranges and check if the RowId is allowed --- .../Game/Text/Evaluator/SeStringEvaluator.cs | 87 ++++++++++++------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index cd981ae75..424b2769a 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1630,37 +1630,56 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator var isNoun = false; - Span cols = stackalloc int[2]; + var colIndex = 0; + Span cols = stackalloc int[8]; cols.Clear(); + var isInRange = false; - if (ranges.StartsWith("noun")) + while (!string.IsNullOrWhiteSpace(ranges)) { - isNoun = true; - } - else if (ranges.StartsWith("col")) - { - var i = 0; - while (i < cols.Length && ranges.StartsWith("col-", StringComparison.Ordinal)) + // find the end of the current entry + var entryEnd = ranges.IndexOf(','); + if (entryEnd == -1) + entryEnd = ranges.Length; + + if (ranges.StartsWith("noun", StringComparison.Ordinal)) { - // find the end of the current "col-#" token - var colRangeEnd = ranges.IndexOf(','); - if (colRangeEnd == -1) - colRangeEnd = ranges.Length; - - // parse the column index - cols[i++] = int.Parse(ranges.AsSpan(4, colRangeEnd - 4)); - - // if it's the end of the string, we're done - if (colRangeEnd == ranges.Length) - break; - - // move to the next entry - ranges = ranges[(colRangeEnd + 1)..].TrimStart(); + isNoun = true; } + else if (ranges.StartsWith("col", StringComparison.Ordinal) && colIndex < cols.Length) + { + cols[colIndex++] = int.Parse(ranges.AsSpan(4, entryEnd - 4)); + } + else if (ranges.StartsWith("tail", StringComparison.Ordinal)) + { + // currently not supported, since there are no known uses + context.Builder.Append(payload); + return false; + } + else + { + var dash = ranges.IndexOf('-'); + if (dash == -1) + { + isInRange |= int.Parse(ranges.AsSpan(0, entryEnd)) == rowId; + } + else + { + isInRange |= rowId >= int.Parse(ranges.AsSpan(0, dash)) + && rowId <= int.Parse(ranges.AsSpan(dash + 1, entryEnd - dash - 1)); + } + } + + // if it's the end of the string, we're done + if (entryEnd == ranges.Length) + break; + + // else, move to the next entry + ranges = ranges[(entryEnd + 1)..].TrimStart(); } - else if (ranges.StartsWith("tail")) + + if (!isInRange) { - // couldn't find any, so we don't handle them :p context.Builder.Append(payload); return false; } @@ -1678,15 +1697,21 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator } else if (this.dataManager.GetExcelSheet(context.Language, sheetName).TryGetRow(rowId, out var row)) { - // the game uses a priority system for columns here. - // if the first column is empty (for example MainCommand.Alias), then the next one is used (MainCommand.Command) - for (var i = 0; i < cols.Length; i++) + if (colIndex == 0) { - var text = row.ReadStringColumn(cols[i]); - if (!text.IsEmpty) + context.Builder.Append(row.ReadStringColumn(0)); + return true; + } + else + { + for (var i = 0; i < colIndex; i++) { - context.Builder.Append(text); - break; + var text = row.ReadStringColumn(cols[i]); + if (!text.IsEmpty) + { + context.Builder.Append(text); + break; + } } } } From c264fb134edcf6299d212f77031030566325bfa7 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Sep 2025 04:09:23 +0200 Subject: [PATCH 048/477] Only check range if it has ranges --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 424b2769a..6ad58ccd8 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1633,6 +1633,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator var colIndex = 0; Span cols = stackalloc int[8]; cols.Clear(); + var hasRanges = false; var isInRange = false; while (!string.IsNullOrWhiteSpace(ranges)) @@ -1659,6 +1660,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator else { var dash = ranges.IndexOf('-'); + + hasRanges |= true; + if (dash == -1) { isInRange |= int.Parse(ranges.AsSpan(0, entryEnd)) == rowId; @@ -1678,7 +1682,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator ranges = ranges[(entryEnd + 1)..].TrimStart(); } - if (!isInRange) + if (hasRanges && !isInRange) { context.Builder.Append(payload); return false; From c03e7ecfe6ea01c556d015d427df60dc0e1739d4 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 21 Sep 2025 02:16:09 +0200 Subject: [PATCH 049/477] Fix Group being off by 1 --- .../Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs index 8035cdd74..7ca1609fa 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs @@ -114,7 +114,7 @@ public class AutoTranslatePayload : Payload, ITextProvider && expr1.TryGetUInt(out var group) && expr2.TryGetUInt(out var key)) { - this.Group = group; + this.Group = group + 1; this.Key = key; } } From 0bb87d87b730e3cc0e0ac87b299837ad31bf1b34 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 21 Sep 2025 02:18:55 +0200 Subject: [PATCH 050/477] Add AutoTranslatePayload re-encode test --- .../Text/SeStringHandling/SeStringTests.cs | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/Dalamud.Test/Game/Text/SeStringHandling/SeStringTests.cs b/Dalamud.Test/Game/Text/SeStringHandling/SeStringTests.cs index 9a48a6615..171031db6 100644 --- a/Dalamud.Test/Game/Text/SeStringHandling/SeStringTests.cs +++ b/Dalamud.Test/Game/Text/SeStringHandling/SeStringTests.cs @@ -1,6 +1,10 @@ -using System; +using System; +using System.IO; + using Dalamud.Configuration; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; + using Xunit; namespace Dalamud.Test.Game.Text.SeStringHandling @@ -50,19 +54,44 @@ namespace Dalamud.Test.Game.Text.SeStringHandling var config = new MockConfig { Text = seString }; PluginConfigurations.SerializeConfig(config); } - + [Fact] public void TestConfigDeserializable() { var builder = new SeStringBuilder(); var seString = builder.AddText("Some text").Build(); var config = new MockConfig { Text = seString }; - + // This relies on the type information being maintained, which is why we're using these // static methods instead of default serialization/deserialization. var configSerialized = PluginConfigurations.SerializeConfig(config); var configDeserialized = (MockConfig)PluginConfigurations.DeserializeConfig(configSerialized); Assert.Equal(config, configDeserialized); } + + [Theory] + [InlineData(49, 209)] + [InlineData(71, 7)] + [InlineData(62, 116)] + public void TestAutoTranslatePayloadReencode(uint group, uint key) + { + var payload = new AutoTranslatePayload(group, key); + + Assert.Equal(group, payload.Group); + Assert.Equal(key, payload.Key); + + var encoded = payload.Encode(); + using var stream = new MemoryStream(encoded); + using var reader = new BinaryReader(stream); + var decodedPayload = Payload.Decode(reader) as AutoTranslatePayload; + + Assert.Equal(group, decodedPayload.Group); + Assert.Equal(key, decodedPayload.Key); + + Assert.Equal(payload.Group, decodedPayload.Group); + Assert.Equal(payload.Key, decodedPayload.Key); + + Assert.Equal(encoded, decodedPayload.Encode()); + } } } From 9b224857f1118c9e3e314e2c86de45aed4ee2dcd Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 21 Sep 2025 03:08:28 +0200 Subject: [PATCH 051/477] Remove redundant checks --- Dalamud.Test/Game/Text/SeStringHandling/SeStringTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Dalamud.Test/Game/Text/SeStringHandling/SeStringTests.cs b/Dalamud.Test/Game/Text/SeStringHandling/SeStringTests.cs index 171031db6..2a19d6216 100644 --- a/Dalamud.Test/Game/Text/SeStringHandling/SeStringTests.cs +++ b/Dalamud.Test/Game/Text/SeStringHandling/SeStringTests.cs @@ -88,9 +88,6 @@ namespace Dalamud.Test.Game.Text.SeStringHandling Assert.Equal(group, decodedPayload.Group); Assert.Equal(key, decodedPayload.Key); - Assert.Equal(payload.Group, decodedPayload.Group); - Assert.Equal(payload.Key, decodedPayload.Key); - Assert.Equal(encoded, decodedPayload.Encode()); } } From 374f9fcbd0c25231eaaf7745d4a822862f238801 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 21 Sep 2025 03:10:00 +0200 Subject: [PATCH 052/477] Add some AutoTranslatePayload.Text self-tests --- .../Steps/SeStringEvaluatorSelfTestStep.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs index 8e66dd5cf..9853e31d4 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs @@ -1,6 +1,9 @@ using Dalamud.Bindings.ImGui; +using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState; using Dalamud.Game.Text.Evaluator; +using Dalamud.Game.Text.SeStringHandling.Payloads; + using Lumina.Text.ReadOnly; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; @@ -75,6 +78,55 @@ internal class SeStringEvaluatorSelfTestStep : ISelfTestStep return SelfTestStepResult.Waiting; } + this.step++; + break; + + case 2: + ImGui.Text("Checking AutoTranslatePayload.Text results..."u8); + + var config = Service.Get(); + var originalLanguageOverride = config.LanguageOverride; + + Span<(string Language, uint Group, uint Key, string ExpectedText)> tests = [ + ("en", 49u, 209u, " albino karakul "), // Mount + ("en", 62u, 116u, " /echo "), // TextCommand - testing Command + ("en", 62u, 143u, " /dutyfinder "), // TextCommand - testing Alias over Command + ("en", 65u, 67u, " Minion of Light "), // Companion - testing noun handling for the german language (special case) + ("en", 71u, 7u, " Phantom Geomancer "), // MKDSupportJob + + ("de", 49u, 209u, " Albino-Karakul "), // Mount + ("de", 62u, 115u, " /freiegesellschaft "), // TextCommand - testing Alias over Command + ("de", 62u, 116u, " /echo "), // TextCommand - testing Command + ("de", 65u, 67u, " Begleiter des Lichts "), // Companion - testing noun handling for the german language (special case) + ("de", 71u, 7u, " Phantom-Geomant "), // MKDSupportJob + ]; + + try + { + foreach (var (language, group, key, expectedText) in tests) + { + config.LanguageOverride = language; + + var payload = new AutoTranslatePayload(group, key); + + if (payload.Text != expectedText) + { + ImGui.Text($"Test failed for Group {group}, Key {key}"); + ImGui.Text($"Expected: {expectedText}"); + ImGui.Text($"Got: {payload.Text}"); + + if (ImGui.Button("Continue"u8)) + return SelfTestStepResult.Fail; + + return SelfTestStepResult.Waiting; + } + } + } + finally + { + config.LanguageOverride = originalLanguageOverride; + } + return SelfTestStepResult.Pass; } From ce1faa50cfc226463903e0cab63c8f3fd96b4ba2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 21 Sep 2025 14:00:23 +0200 Subject: [PATCH 053/477] Remove AutoTranslatePayload text cache - Lumina lookups aren't that taxing anymore - Cache wasn't invalidated when the language override changes --- .../Text/SeStringHandling/Payloads/AutoTranslatePayload.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs index 7ca1609fa..470e942c3 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs @@ -14,7 +14,6 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads; /// public class AutoTranslatePayload : Payload, ITextProvider { - private string? text; private ReadOnlySeString payload; /// @@ -77,11 +76,11 @@ public class AutoTranslatePayload : Payload, ITextProvider { if (this.Group is 100 or 200) { - return this.text ??= Service.Get().Evaluate(this.payload).ToString(); + return Service.Get().Evaluate(this.payload).ToString(); } - // wrap the text in the colored brackets that is uses in-game, since those are not actually part of any of the payloads - return this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {Service.Get().Evaluate(this.payload)} {(char)SeIconChar.AutoTranslateClose}"; + // wrap the text in the colored brackets that are used in-game, since those are not actually part of any of the fixed macro payload + return $"{(char)SeIconChar.AutoTranslateOpen} {Service.Get().Evaluate(this.payload)} {(char)SeIconChar.AutoTranslateClose}"; } } From 3d0790e650d60248f9c24de693aeb5b11ac8d59b Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Mon, 22 Sep 2025 05:23:03 +0200 Subject: [PATCH 054/477] Update Excel Schema (#2408) Co-authored-by: github-actions[bot] --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 1c7d19628..fae331428 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 1c7d196283f12210e000e8dfd3cc1226fcf39e41 +Subproject commit fae3314282b38efc2ce1edf0255d4357d924ced1 From c38365bc99792dc16c5e16f160f8a1985dcb002c Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Mon, 22 Sep 2025 05:23:53 +0200 Subject: [PATCH 055/477] Update ClientStructs (#2398) 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 3ae5a65f6..39ca6265c 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 3ae5a65f6d3a00d2d1091319ddec1442ea22726a +Subproject commit 39ca6265c77a8c9fc36052b79f2daa9ca7af0c69 From 0b3a5a713e7352f15c138f6161a0b9d60272cad7 Mon Sep 17 00:00:00 2001 From: Caraxi Date: Mon, 22 Sep 2025 12:54:48 +0930 Subject: [PATCH 056/477] enforce limits on alpha slider (#2412) --- Dalamud/Interface/Windowing/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index d302552f5..d4f0070db 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -523,7 +523,7 @@ public abstract class Window if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f, 100f)) { - this.internalAlpha = alpha / 100f; + this.internalAlpha = Math.Clamp(alpha / 100f, 0.2f, 1f); this.presetDirty = true; } From 9b0c275b8b143aded0fcb556fede9c2edb676fc9 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 22 Sep 2025 23:29:40 +0200 Subject: [PATCH 057/477] Fix ExpressionType.Weekday being off by 1 --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 6ad58ccd8..4fd42efb3 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -2061,7 +2061,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator value = (uint)MacroDecoder.GetMacroTime()->tm_mday; return true; case ExpressionType.Weekday: - value = (uint)MacroDecoder.GetMacroTime()->tm_wday; + value = (uint)MacroDecoder.GetMacroTime()->tm_wday + 1; return true; case ExpressionType.Month: value = (uint)MacroDecoder.GetMacroTime()->tm_mon + 1; From e2e3a01cc3ba63f172d875e437ef99af49b2db90 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 22 Sep 2025 23:33:30 +0200 Subject: [PATCH 058/477] Fix SeString Creator input length cutting off long macro strings --- .../Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index d2d0cb3b2..a153d3c2f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -696,7 +696,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget ImGui.TableNextColumn(); // Text var message = entry.Message; ImGui.SetNextItemWidth(-1); - if (ImGui.InputText($"##{i}_Message", ref message, 255)) + if (ImGui.InputText($"##{i}_Message", ref message, 2048)) { entry.Message = message; updateString |= true; From 9447708058ce94d0a7a0c75a928ef6bcd7ccb6e3 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 22 Sep 2025 23:36:39 +0200 Subject: [PATCH 059/477] Fix Copy MacroString button --- .../Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index a153d3c2f..465d64370 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -510,7 +510,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget } } - ImGui.SetClipboardText(sb.ToReadOnlySeString().ToString()); + ImGui.SetClipboardText(sb.ToReadOnlySeString().ToMacroString()); } ImGui.SameLine(); From 69a8bdd63808eaf5bed8be5628711b1e2aa168e1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 22 Sep 2025 23:37:27 +0200 Subject: [PATCH 060/477] Update GlobalParameters list --- .../Data/Widgets/SeStringCreatorWidget.cs | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index 465d64370..4f2369e3b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -4,7 +4,6 @@ using System.Numerics; using System.Text; using Dalamud.Bindings.ImGui; -using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; @@ -13,12 +12,13 @@ using Dalamud.Game.Text.Noun.Enums; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using Dalamud.Memory; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.Text; + using Lumina.Data; using Lumina.Data.Files.Excel; using Lumina.Data.Structs.Excel; @@ -313,13 +313,13 @@ internal class SeStringCreatorWidget : IDataWindowWidget ImGui.Text(i switch { 0 => "Player Name", - 1 => "Temp Player 1 Name", - 2 => "Temp Player 2 Name", + 1 => "Temp Entity 1: Name", + 2 => "Temp Entity 2: Name", 3 => "Player Sex", - 4 => "Temp Player 1 Sex", - 5 => "Temp Player 2 Sex", - 6 => "Temp Player 1 Unk 1", - 7 => "Temp Player 2 Unk 1", + 4 => "Temp Entity 1: Sex", + 5 => "Temp Entity 2: Sex", + 6 => "Temp Entity 1: ObjStrId", + 7 => "Temp Entity 2: ObjStrId", 10 => "Eorzea Time Hours", 11 => "Eorzea Time Minutes", 12 => "ColorSay", @@ -368,14 +368,19 @@ internal class SeStringCreatorWidget : IDataWindowWidget 62 => "ColorLoot", 63 => "ColorCraft", 64 => "ColorGathering", - 65 => "Temp Player 1 Unk 2", - 66 => "Temp Player 2 Unk 2", + 65 => "Temp Entity 1: Name starts with Vowel", + 66 => "Temp Entity 2: Name starts with Vowel", 67 => "Player ClassJobId", 68 => "Player Level", + 69 => "Player StartTown", 70 => "Player Race", 71 => "Player Synced Level", - 77 => "Client/Plattform?", + 73 => "Quest#66047: Has met Alphinaud and Alisaie", + 74 => "PlayStation Generation", + 75 => "Is Legacy Player", + 77 => "Client/Platform?", 78 => "Player BirthMonth", + 79 => "PadMode", 82 => "Datacenter Region", 83 => "ColorCWLS2", 84 => "ColorCWLS3", @@ -396,6 +401,11 @@ internal class SeStringCreatorWidget : IDataWindowWidget 100 => "LogSetRoleColor 1: LogColorOtherClass", 101 => "LogSetRoleColor 2: LogColorOtherClass", 102 => "Has Login Security Token", + 103 => "Is subscribed to PlayStation Plus", + 104 => "PadMouseMode", + 106 => "Preferred World Bonus Max Level", + 107 => "Occult Crescent Support Job Level", + 108 => "Deep Dungeon Id", _ => string.Empty, }); } From 2625f51021b1d170eabb556004d0f97f71aa40a1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 22 Sep 2025 23:55:16 +0200 Subject: [PATCH 061/477] Fix missing macro strings in AddFromSheetPopup --- .../Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index 4f2369e3b..86225a63b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -635,7 +635,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget ImGui.Text(i.ToString()); ImGui.TableNextColumn(); - if (ImGui.Selectable($"{value.ToString().Truncate(100)}###Column{i}")) + if (ImGui.Selectable($"{value.ToMacroString().Truncate(100)}###Column{i}")) { foreach (var payload in value) { From dceeccb242332e33c2f40b5336018a42308a5a39 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 22 Sep 2025 23:56:28 +0200 Subject: [PATCH 062/477] Load valid sheets for AddFromSheetPopup asynchronously --- .../Data/Widgets/SeStringCreatorWidget.cs | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index 86225a63b..a88f576f9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; +using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Data; @@ -146,6 +147,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget private SeStringParameter[]? localParameters = [Util.GetScmVersion()]; private ReadOnlySeString input; private ClientLanguage? language; + private Task? validImportSheetNamesTask; private int importSelectedSheetName; private int importRowId; private string[]? validImportSheetNames; @@ -565,22 +567,31 @@ internal class SeStringCreatorWidget : IDataWindowWidget var dataManager = Service.Get(); - this.validImportSheetNames ??= dataManager.Excel.SheetNames.Where(sheetName => + this.validImportSheetNamesTask ??= Task.Run(() => { - try + this.validImportSheetNames = dataManager.Excel.SheetNames.Where(sheetName => { - var headerFile = dataManager.GameData.GetFile($"exd/{sheetName}.exh"); - if (headerFile.Header.Variant != ExcelVariant.Default) - return false; + try + { + var headerFile = dataManager.GameData.GetFile($"exd/{sheetName}.exh"); + if (headerFile.Header.Variant != ExcelVariant.Default) + return false; - var sheet = dataManager.Excel.GetSheet(Language.English, sheetName); - return sheet.Columns.Any(col => col.Type == ExcelColumnDataType.String); - } - catch - { - return false; - } - }).OrderBy(sheetName => sheetName, StringComparer.InvariantCulture).ToArray(); + var sheet = dataManager.Excel.GetSheet(Language.English, sheetName); + return sheet.Columns.Any(col => col.Type == ExcelColumnDataType.String); + } + catch + { + return false; + } + }).OrderBy(sheetName => sheetName, StringComparer.InvariantCulture).ToArray(); + }); + + if (this.validImportSheetNames == null) + { + ImGui.Text("Loading sheets..."u8); + return; + } var sheetChanged = ImGui.Combo("Sheet Name", ref this.importSelectedSheetName, this.validImportSheetNames); From 1633e68b76ef859286c26387746e09360ffb3cac Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 23 Sep 2025 13:16:39 +0200 Subject: [PATCH 063/477] Fix/simplify range handling --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 4fd42efb3..9f898bcca 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1643,13 +1643,15 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (entryEnd == -1) entryEnd = ranges.Length; + var entry = ranges.AsSpan(0, entryEnd); + if (ranges.StartsWith("noun", StringComparison.Ordinal)) { isNoun = true; } else if (ranges.StartsWith("col", StringComparison.Ordinal) && colIndex < cols.Length) { - cols[colIndex++] = int.Parse(ranges.AsSpan(4, entryEnd - 4)); + cols[colIndex++] = int.Parse(entry[4..]); } else if (ranges.StartsWith("tail", StringComparison.Ordinal)) { @@ -1659,18 +1661,18 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator } else { - var dash = ranges.IndexOf('-'); + var dash = entry.IndexOf('-'); hasRanges |= true; if (dash == -1) { - isInRange |= int.Parse(ranges.AsSpan(0, entryEnd)) == rowId; + isInRange |= int.Parse(entry) == rowId; } else { - isInRange |= rowId >= int.Parse(ranges.AsSpan(0, dash)) - && rowId <= int.Parse(ranges.AsSpan(dash + 1, entryEnd - dash - 1)); + isInRange |= rowId >= int.Parse(entry[..dash]) + && rowId <= int.Parse(entry[(dash + 1)..]); } } From 191aa8d6967c954ee1e43ae599e0302c2c833137 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 28 Sep 2025 15:31:46 +0200 Subject: [PATCH 064/477] Move IsAvailableForTesting to IPluginManifest --- .../Internal/PluginCategoryManager.cs | 2 +- .../PluginInstaller/PluginInstallerWindow.cs | 6 +-- Dalamud/Plugin/Internal/PluginManager.cs | 45 +++++++------------ .../Types/Manifest/IPluginManifest.cs | 7 ++- .../Types/Manifest/RemotePluginManifest.cs | 5 --- .../Plugin/Internal/Types/PluginManifest.cs | 6 +++ 6 files changed, 30 insertions(+), 41 deletions(-) diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index 71c869ede..d3aea7f57 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -294,7 +294,7 @@ internal class PluginCategoryManager } } - if (PluginManager.HasTestingVersion(manifest) || manifest.IsTestingExclusive) + if (manifest.IsTestingExclusive || manifest.IsAvailableForTesting) categoryList.Add(CategoryKind.AvailableForTesting); // always add, even if empty diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 946a5f19a..dc41e9588 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2593,8 +2593,7 @@ internal class PluginInstallerWindow : Window, IDisposable var configuration = Service.Get(); var pluginManager = Service.Get(); - var hasTestingVersionAvailable = configuration.DoPluginTest && - PluginManager.HasTestingVersion(manifest); + var hasTestingVersionAvailable = configuration.DoPluginTest && manifest.IsAvailableForTesting; if (ImGui.BeginPopupContextItem("ItemContextMenu"u8)) { @@ -2689,8 +2688,7 @@ internal class PluginInstallerWindow : Window, IDisposable label += Locs.PluginTitleMod_TestingVersion; } - var hasTestingAvailable = this.pluginListAvailable.Any(x => x.InternalName == plugin.InternalName && - x.IsAvailableForTesting); + var hasTestingAvailable = this.pluginListAvailable.Any(x => x.InternalName == plugin.InternalName && x.IsAvailableForTesting); if (hasTestingAvailable && configuration.DoPluginTest && testingOptIn == null) { label += Locs.PluginTitleMod_TestingAvailable; diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index db803caa8..52c7689a8 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -278,26 +278,6 @@ internal class PluginManager : IInternalDisposableService return !manifest.IsHide; } - /// - /// Check if a manifest even has an available testing version. - /// - /// The manifest to test. - /// Whether a testing version is available. - public static bool HasTestingVersion(IPluginManifest manifest) - { - var av = manifest.AssemblyVersion; - var tv = manifest.TestingAssemblyVersion; - var hasTv = tv != null; - - if (hasTv) - { - return tv > av && - manifest.TestingDalamudApiLevel == DalamudApiLevel; - } - - return false; - } - /// /// Get a disposable that will lock plugin lists while it is not disposed. /// You must NEVER use this in async code. @@ -371,6 +351,20 @@ internal class PluginManager : IInternalDisposableService return this.configuration.PluginTestingOptIns!.Any(x => x.InternalName == manifest.InternalName); } + /// + /// For a given manifest, determine if the testing version can be used over the normal version. + /// The higher of the two versions is calculated after checking other settings. + /// + /// Manifest to check. + /// A value indicating whether testing can be used. + public bool CanUseTesting(IPluginManifest manifest) + { + if (!this.configuration.DoPluginTest) + return false; + + return manifest.IsTestingExclusive || manifest.IsAvailableForTesting; + } + /// /// For a given manifest, determine if the testing version should be used over the normal version. /// The higher of the two versions is calculated after checking other settings. @@ -379,16 +373,7 @@ internal class PluginManager : IInternalDisposableService /// A value indicating whether testing should be used. public bool UseTesting(IPluginManifest manifest) { - if (!this.configuration.DoPluginTest) - return false; - - if (!this.HasTestingOptIn(manifest)) - return false; - - if (manifest.IsTestingExclusive) - return true; - - return HasTestingVersion(manifest); + return this.CanUseTesting(manifest) && this.HasTestingOptIn(manifest); } /// diff --git a/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs index 5ab5abbc1..c770c80be 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Dalamud.Plugin.Internal.Types.Manifest; @@ -118,4 +118,9 @@ public interface IPluginManifest /// Gets an URL for the plugin's icon. /// public string? IconUrl { get; } + + /// + /// Gets a value indicating whether this plugin is eligible for testing. + /// + public bool IsAvailableForTesting { get; } } diff --git a/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs index e3d99a85a..47e92cd84 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs @@ -21,9 +21,4 @@ internal record RemotePluginManifest : PluginManifest /// Gets or sets the changelog to be shown when obtaining the testing version of the plugin. /// public string? TestingChangelog { get; set; } - - /// - /// Gets a value indicating whether this plugin is eligible for testing. - /// - public bool IsAvailableForTesting => this.TestingAssemblyVersion != null && this.TestingAssemblyVersion > this.AssemblyVersion; } diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index 57001d63b..f4545d455 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -160,4 +160,10 @@ internal record PluginManifest : IPluginManifest /// [JsonProperty("_Dip17Channel")] public string? Dip17Channel { get; init; } + + /// + public bool IsAvailableForTesting + => this.TestingAssemblyVersion != null && + this.TestingAssemblyVersion > this.AssemblyVersion && + this.TestingDalamudApiLevel == PluginManager.DalamudApiLevel; } From d1fbee2829b6d652abe09eaaeb2290653ec1c87a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 28 Sep 2025 15:39:36 +0200 Subject: [PATCH 065/477] Remove manifest API filter in installer The API is already checked in `PluginManager.IsManifestEligible`, so a plugin not matching it doesn't even get here. --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index dc41e9588..3e56b65c9 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3782,16 +3782,7 @@ internal class PluginInstallerWindow : Window, IDisposable private bool IsManifestFiltered(IPluginManifest manifest) { - var hasSearchString = !string.IsNullOrWhiteSpace(this.searchText); - var oldApi = (manifest.TestingDalamudApiLevel == null - || manifest.TestingDalamudApiLevel < PluginManager.DalamudApiLevel) - && manifest.DalamudApiLevel < PluginManager.DalamudApiLevel; - var installed = this.IsManifestInstalled(manifest).IsInstalled; - - if (oldApi && !hasSearchString && !installed) - return true; - - if (!hasSearchString) + if (string.IsNullOrWhiteSpace(this.searchText)) return false; return this.GetManifestSearchScore(manifest) < 1; From 8edbc0ee78f2ff944bcc663cdef35024253a81be Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 28 Sep 2025 15:59:08 +0200 Subject: [PATCH 066/477] Ignore user testing opt-in for manifest eligibility check --- Dalamud/Plugin/Internal/PluginManager.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 52c7689a8..8292ac20c 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1193,9 +1193,18 @@ internal class PluginManager : IInternalDisposableService return false; // API level - we keep the API before this in the installer to show as "outdated" - var effectiveApiLevel = this.UseTesting(manifest) && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel; - if (effectiveApiLevel < DalamudApiLevel - 1 && !this.LoadAllApiLevels) - return false; + if (!this.LoadAllApiLevels) + { + var effectiveDalamudApiLevel = + this.CanUseTesting(manifest) && + manifest.TestingDalamudApiLevel.HasValue && + manifest.TestingDalamudApiLevel.Value > manifest.DalamudApiLevel + ? manifest.TestingDalamudApiLevel.Value + : manifest.DalamudApiLevel; + + if (effectiveDalamudApiLevel < PluginManager.DalamudApiLevel - 1) + return false; + } // Banned if (this.IsManifestBanned(manifest)) From 87adb2dfb7933b3332870ea7314b88f4ec76cb7c Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 28 Sep 2025 16:00:09 +0200 Subject: [PATCH 067/477] More expressive code --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 5 +++-- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 3e56b65c9..b203b3894 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2454,10 +2454,11 @@ internal class PluginInstallerWindow : Window, IDisposable var configuration = Service.Get(); var pluginManager = Service.Get(); + var canUseTesting = pluginManager.CanUseTesting(manifest); var useTesting = pluginManager.UseTesting(manifest); var wasSeen = this.WasPluginSeen(manifest.InternalName); - var effectiveApiLevel = useTesting && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel; + var effectiveApiLevel = useTesting ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel; var isOutdated = effectiveApiLevel < PluginManager.DalamudApiLevel; var isIncompatible = manifest.MinimumDalamudVersion != null && @@ -2487,7 +2488,7 @@ internal class PluginInstallerWindow : Window, IDisposable { label += Locs.PluginTitleMod_TestingExclusive; } - else if (configuration.DoPluginTest && PluginManager.HasTestingVersion(manifest)) + else if (canUseTesting) { label += Locs.PluginTitleMod_TestingAvailable; } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 70b1db872..da8ec8ff9 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -301,7 +301,7 @@ internal class LocalPlugin : IAsyncDisposable throw new PluginPreconditionFailedException($"Unable to load {this.Name}, game is newer than applicable version {this.manifest.ApplicableVersion}"); // We want to allow loading dev plugins with a lower API level than the current Dalamud API level, for ease of development - if (this.manifest.EffectiveApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels && !this.IsDev) + if (!pluginManager.LoadAllApiLevels && !this.IsDev && this.manifest.EffectiveApiLevel < PluginManager.DalamudApiLevel) throw new PluginPreconditionFailedException($"Unable to load {this.Name}, incompatible API level {this.manifest.EffectiveApiLevel}"); // We might want to throw here? From 9c5e4f5a32be0ab6d2702a5243fcb4d1f82e39ce Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 28 Sep 2025 16:21:25 +0200 Subject: [PATCH 068/477] Add JsonIgnore attribute on IsAvailableForTesting --- Dalamud/Plugin/Internal/Types/PluginManifest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index f4545d455..cbf69bb5e 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -162,6 +162,7 @@ internal record PluginManifest : IPluginManifest public string? Dip17Channel { get; init; } /// + [JsonIgnore] public bool IsAvailableForTesting => this.TestingAssemblyVersion != null && this.TestingAssemblyVersion > this.AssemblyVersion && From 9091216e1c5b32097224e9a57c088b959589d47c Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 29 Sep 2025 18:02:53 +0200 Subject: [PATCH 069/477] Update ClientState (#2410) * Add MapChanged event to ClientState * Add PublicInstanceId with event to ClientState * Set eventhandlers to null * Rework events and add ZoneInit event --- Dalamud/Game/ClientState/ClientState.cs | 214 ++++++++++++++---- .../ClientState/ClientStateAddressResolver.cs | 16 +- Dalamud/Game/ClientState/ZoneInit.cs | 90 ++++++++ Dalamud/Plugin/Services/IClientState.cs | 21 ++ 4 files changed, 286 insertions(+), 55 deletions(-) create mode 100644 Dalamud/Game/ClientState/ZoneInit.cs diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 407b54060..e92af21c3 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -15,8 +15,8 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.Network; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -36,9 +36,9 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; - private readonly Hook setupTerritoryTypeHook; + private readonly Hook handleZoneInitPacketHook; private readonly Hook uiModuleHandlePacketHook; - private Hook onLogoutHook; + private readonly Hook setCurrentInstanceHook; [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -46,6 +46,12 @@ internal sealed class ClientState : IInternalDisposableService, IClientState [ServiceManager.ServiceDependency] private readonly NetworkHandlers networkHandlers = Service.Get(); + private Hook onLogoutHook; + private bool initialized; + private ushort territoryTypeId; + private bool isPvP; + private uint mapId; + private uint instance; private bool lastConditionNone = true; [ServiceManager.ServiceConstructor] @@ -59,26 +65,37 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language; - var setTerritoryTypeAddr = EventFramework.Addresses.SetTerritoryTypeId.Value; - Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(setTerritoryTypeAddr)}"); - - this.setupTerritoryTypeHook = Hook.FromAddress(setTerritoryTypeAddr, this.SetupTerritoryTypeDetour); + this.handleZoneInitPacketHook = Hook.FromAddress(this.AddressResolver.HandleZoneInitPacket, this.HandleZoneInitPacketDetour); this.uiModuleHandlePacketHook = Hook.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour); + this.setCurrentInstanceHook = Hook.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour); - this.framework.Update += this.FrameworkOnOnUpdateEvent; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; - this.setupTerritoryTypeHook.Enable(); + this.handleZoneInitPacketHook.Enable(); this.uiModuleHandlePacketHook.Enable(); + this.setCurrentInstanceHook.Enable(); this.framework.RunOnTick(this.Setup); } private unsafe delegate void ProcessPacketPlayerSetupDelegate(nint a1, nint packet); + private unsafe delegate void HandleZoneInitPacketDelegate(nint a1, uint localPlayerEntityId, nint packet, byte type); + + private unsafe delegate void SetCurrentInstanceDelegate(NetworkModuleProxy* thisPtr, short instanceId); + + /// + public event Action ZoneInit; + /// public event Action? TerritoryChanged; + /// + public event Action? MapIdChanged; + + /// + public event Action? InstanceChanged; + /// public event IClientState.ClassJobChangeDelegate? ClassJobChanged; @@ -104,15 +121,65 @@ internal sealed class ClientState : IInternalDisposableService, IClientState public ClientLanguage ClientLanguage { get; } /// - public ushort TerritoryType { get; private set; } + public ushort TerritoryType + { + get => this.territoryTypeId; + private set + { + if (this.territoryTypeId != value) + { + this.territoryTypeId = value; + + if (this.initialized) + { + Log.Debug("TerritoryType changed: {0}", value); + this.TerritoryChanged?.InvokeSafely(value); + } + + var rowRef = LuminaUtils.CreateRef(value); + if (rowRef.IsValid) + { + this.IsPvP = rowRef.Value.IsPvpZone; + } + } + } + } /// - public unsafe uint MapId + public uint MapId { - get + get => this.mapId; + private set { - var agentMap = AgentMap.Instance(); - return agentMap != null ? agentMap->CurrentMapId : 0; + if (this.mapId != value) + { + this.mapId = value; + + if (this.initialized) + { + Log.Debug("MapId changed: {0}", value); + this.MapIdChanged?.InvokeSafely(value); + } + } + } + } + + /// + public uint Instance + { + get => this.instance; + private set + { + if (this.instance != value) + { + this.instance = value; + + if (this.initialized) + { + Log.Debug("Instance changed: {0}", value); + this.InstanceChanged?.InvokeSafely(value); + } + } } } @@ -133,7 +200,31 @@ internal sealed class ClientState : IInternalDisposableService, IClientState } /// - public bool IsPvP { get; private set; } + public bool IsPvP + { + get => this.isPvP; + private set + { + if (this.isPvP != value) + { + this.isPvP = value; + + if (this.initialized) + { + if (value) + { + Log.Debug("EnterPvP"); + this.EnterPvP?.InvokeSafely(); + } + else + { + Log.Debug("LeavePvP"); + this.LeavePvP?.InvokeSafely(); + } + } + } + } + } /// public bool IsPvPExcludingDen => this.IsPvP && this.TerritoryType != 250; @@ -172,11 +263,12 @@ internal sealed class ClientState : IInternalDisposableService, IClientState /// void IInternalDisposableService.DisposeService() { - this.setupTerritoryTypeHook.Dispose(); + this.handleZoneInitPacketHook.Dispose(); this.uiModuleHandlePacketHook.Dispose(); this.onLogoutHook.Dispose(); + this.setCurrentInstanceHook.Dispose(); - this.framework.Update -= this.FrameworkOnOnUpdateEvent; + this.framework.Update -= this.OnFrameworkUpdate; this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; } @@ -186,43 +278,28 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.onLogoutHook.Enable(); this.TerritoryType = (ushort)GameMain.Instance()->CurrentTerritoryTypeId; + this.MapId = AgentMap.Instance()->CurrentMapId; + this.Instance = UIState.Instance()->PublicInstance.InstanceId; + + this.initialized = true; + + this.framework.Update += this.OnFrameworkUpdate; } - private unsafe void SetupTerritoryTypeDetour(EventFramework* eventFramework, ushort territoryType) + private void HandleZoneInitPacketDetour(nint a1, uint localPlayerEntityId, nint packet, byte type) { - this.SetTerritoryType(territoryType); - this.setupTerritoryTypeHook.Original(eventFramework, territoryType); - } + this.handleZoneInitPacketHook.Original(a1, localPlayerEntityId, packet, type); - private unsafe void SetTerritoryType(ushort territoryType) - { - if (this.TerritoryType == territoryType) - return; - - Log.Debug("TerritoryType changed: {0}", territoryType); - - this.TerritoryType = territoryType; - this.TerritoryChanged?.InvokeSafely(territoryType); - - var rowRef = LuminaUtils.CreateRef(territoryType); - if (rowRef.IsValid) + try { - var isPvP = rowRef.Value.IsPvpZone; - if (isPvP != this.IsPvP) - { - this.IsPvP = isPvP; - - if (this.IsPvP) - { - Log.Debug("EnterPvP"); - this.EnterPvP?.InvokeSafely(); - } - else - { - Log.Debug("LeavePvP"); - this.LeavePvP?.InvokeSafely(); - } - } + var eventArgs = ZoneInitEventArgs.Read(packet); + Log.Debug($"ZoneInit: {eventArgs}"); + this.ZoneInit?.InvokeSafely(eventArgs); + this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId; + } + catch (Exception ex) + { + Log.Error(ex, "Exception during ZoneInit"); } } @@ -274,8 +351,16 @@ internal sealed class ClientState : IInternalDisposableService, IClientState } } - private void FrameworkOnOnUpdateEvent(IFramework framework1) + private unsafe void SetCurrentInstanceDetour(NetworkModuleProxy* thisPtr, short instanceId) { + this.setCurrentInstanceHook.Original(thisPtr, instanceId); + this.Instance = (uint)instanceId; + } + + private unsafe void OnFrameworkUpdate(IFramework framework) + { + this.MapId = AgentMap.Instance()->CurrentMapId; + var condition = Service.GetNullable(); var gameGui = Service.GetNullable(); var data = Service.GetNullable(); @@ -357,7 +442,10 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat /// internal ClientStatePluginScoped() { + this.clientStateService.ZoneInit += this.ZoneInitForward; this.clientStateService.TerritoryChanged += this.TerritoryChangedForward; + this.clientStateService.MapIdChanged += this.MapIdChangedForward; + this.clientStateService.InstanceChanged += this.InstanceChangedForward; this.clientStateService.ClassJobChanged += this.ClassJobChangedForward; this.clientStateService.LevelChanged += this.LevelChangedForward; this.clientStateService.Login += this.LoginForward; @@ -367,9 +455,18 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat this.clientStateService.CfPop += this.ContentFinderPopForward; } + /// + public event Action ZoneInit; + /// public event Action? TerritoryChanged; + /// + public event Action? MapIdChanged; + + /// + public event Action? InstanceChanged; + /// public event IClientState.ClassJobChangeDelegate? ClassJobChanged; @@ -400,6 +497,9 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat /// public uint MapId => this.clientStateService.MapId; + /// + public uint Instance => this.clientStateService.Instance; + /// public IPlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer; @@ -427,7 +527,10 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat /// void IInternalDisposableService.DisposeService() { + this.clientStateService.ZoneInit -= this.ZoneInitForward; this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; + this.clientStateService.MapIdChanged -= this.MapIdChangedForward; + this.clientStateService.InstanceChanged -= this.InstanceChangedForward; this.clientStateService.ClassJobChanged -= this.ClassJobChangedForward; this.clientStateService.LevelChanged -= this.LevelChangedForward; this.clientStateService.Login -= this.LoginForward; @@ -436,7 +539,12 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat this.clientStateService.LeavePvP -= this.ExitPvPForward; this.clientStateService.CfPop -= this.ContentFinderPopForward; + this.ZoneInit = null; this.TerritoryChanged = null; + this.MapIdChanged = null; + this.InstanceChanged = null; + this.ClassJobChanged = null; + this.LevelChanged = null; this.Login = null; this.Logout = null; this.EnterPvP = null; @@ -444,8 +552,14 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat this.CfPop = null; } + private void ZoneInitForward(ZoneInitEventArgs eventArgs) => this.ZoneInit?.Invoke(eventArgs); + private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId); + private void MapIdChangedForward(uint mapId) => this.MapIdChanged?.Invoke(mapId); + + private void InstanceChangedForward(uint instanceId) => this.InstanceChanged?.Invoke(instanceId); + private void ClassJobChangedForward(uint classJobId) => this.ClassJobChanged?.Invoke(classJobId); private void LevelChangedForward(uint classJobId, uint level) => this.LevelChanged?.Invoke(classJobId, level); diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 97bc5dae1..2fc859d09 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -10,19 +10,24 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver /// /// Gets the address of the keyboard state. /// - public IntPtr KeyboardState { get; private set; } + public nint KeyboardState { get; private set; } /// /// Gets the address of the keyboard state index array which translates the VK enumeration to the key state. /// - public IntPtr KeyboardStateIndexArray { get; private set; } + public nint KeyboardStateIndexArray { get; private set; } // Functions /// - /// Gets the address of the method which sets up the player. + /// Gets the address of the method that handles the ZoneInit packet. /// - public IntPtr ProcessPacketPlayerSetup { get; private set; } + public nint HandleZoneInitPacket { get; private set; } + + /// + /// Gets the address of the method that sets the current public instance. + /// + public nint SetCurrentInstance { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -30,7 +35,8 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver /// The signature scanner to facilitate setup. protected override void Setup64Bit(ISigScanner sig) { - this.ProcessPacketPlayerSetup = sig.ScanText("40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B D3"); // not in cs struct + this.HandleZoneInitPacket = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 45"); + this.SetCurrentInstance = sig.ScanText("E8 ?? ?? ?? ?? 0F B6 55 ?? 48 8D 0D ?? ?? ?? ?? C0 EA"); // NetworkModuleProxy.SetCurrentInstance // These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used. // lea rcx, ds:1DB9F74h[rax*4] KeyboardState diff --git a/Dalamud/Game/ClientState/ZoneInit.cs b/Dalamud/Game/ClientState/ZoneInit.cs new file mode 100644 index 000000000..5c2213c90 --- /dev/null +++ b/Dalamud/Game/ClientState/ZoneInit.cs @@ -0,0 +1,90 @@ +using System.Linq; +using System.Text; + +using Dalamud.Data; + +using Lumina.Excel.Sheets; + +namespace Dalamud.Game.ClientState; + +/// +/// Provides event data for when the game should initialize a zone. +/// +public class ZoneInitEventArgs : EventArgs +{ + /// + /// Gets the territory type of the zone being entered. + /// + public TerritoryType TerritoryType { get; private set; } + + /// + /// Gets the instance number of the zone, used when multiple copies of an area are active. + /// + public ushort Instance { get; private set; } + + /// + /// Gets the associated content finder condition for the zone, if any. + /// + public ContentFinderCondition ContentFinderCondition { get; private set; } + + /// + /// Gets the current weather in the zone upon entry. + /// + public Weather Weather { get; private set; } + + /// + /// Gets the set of active festivals in the zone. + /// + public Festival[] ActiveFestivals { get; private set; } = []; + + /// + /// Gets the phases corresponding to the active festivals. + /// + public ushort[] ActiveFestivalPhases { get; private set; } = []; + + /// + /// Reads raw zone initialization data from a network packet and constructs the event arguments. + /// + /// A pointer to the raw packet data. + /// A populated from the packet. + public static unsafe ZoneInitEventArgs Read(nint packet) + { + var dataManager = Service.Get(); + var eventArgs = new ZoneInitEventArgs(); + + var flags = *(byte*)(packet + 0x12); + + eventArgs.TerritoryType = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x02)); + eventArgs.Instance = flags >= 0 ? (ushort)0 : *(ushort*)(packet + 0x04); + eventArgs.ContentFinderCondition = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x06)); + eventArgs.Weather = dataManager.GetExcelSheet().GetRow(*(byte*)(packet + 0x10)); + + const int NumFestivals = 4; + eventArgs.ActiveFestivals = new Festival[NumFestivals]; + eventArgs.ActiveFestivalPhases = new ushort[NumFestivals]; + + // There are also 4 festival ids and phases for PlayerState at +0x3E and +0x46 respectively, + // but it's unclear why they exist as separate entries and why they would be different. + for (var i = 0; i < NumFestivals; i++) + { + eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x2E + (i * 2))); + eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2)); + } + + return eventArgs; + } + + /// + public override string ToString() + { + var sb = new StringBuilder("ZoneInitEventArgs { "); + sb.Append($"TerritoryTypeId = {this.TerritoryType.RowId}, "); + sb.Append($"Instance = {this.Instance}, "); + sb.Append($"ContentFinderCondition = {this.ContentFinderCondition.RowId}, "); + sb.Append($"Weather = {this.Weather.RowId}, "); + sb.Append($"ActiveFestivals = [{string.Join(", ", this.ActiveFestivals.Select(f => f.RowId))}], "); + sb.Append($"ActiveFestivalPhases = [{string.Join(", ", this.ActiveFestivalPhases)}]"); + sb.Append(" }"); + return sb.ToString(); + } +} diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 60d8a17e2..0342ea77c 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -1,4 +1,5 @@ using Dalamud.Game; +using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; @@ -29,11 +30,26 @@ public interface IClientState /// The success/failure code. public delegate void LogoutDelegate(int type, int code); + /// + /// Event that gets fired when the game initializes a zone. + /// + public event Action ZoneInit; + /// /// Event that gets fired when the current Territory changes. /// public event Action TerritoryChanged; + /// + /// Event that gets fired when the current Map changes. + /// + public event Action MapIdChanged; + + /// + /// Event that gets fired when the current zone Instance changes. + /// + public event Action InstanceChanged; + /// /// Event that fires when a characters ClassJob changed. /// @@ -85,6 +101,11 @@ public interface IClientState /// public uint MapId { get; } + /// + /// Gets the instance number of the current zone, used when multiple copies of an area are active. + /// + public uint Instance { get; } + /// /// Gets the local player character, if one is present. /// From efaff769b5e862a583145ccf4685184acfe2afaf Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 29 Sep 2025 18:06:28 +0200 Subject: [PATCH 070/477] Rename Id fields to match CS names (#2405) * Rename DataId to BaseId * Fix obsoletes * Inherit documentation --- Dalamud/Game/ClientState/Buddy/BuddyMember.cs | 9 +++ .../ClientState/Objects/Types/GameObject.cs | 9 +++ Dalamud/Game/ClientState/Party/PartyMember.cs | 78 +++++++------------ .../Windows/Data/Widgets/BuddyListWidget.cs | 6 +- Dalamud/Utility/Util.cs | 4 +- 5 files changed, 49 insertions(+), 57 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs index 025de611d..393598d32 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs @@ -19,8 +19,14 @@ public interface IBuddyMember /// /// Gets the object ID of this buddy. /// + [Obsolete("Renamed to EntityId")] uint ObjectId { get; } + /// + /// Gets the entity ID of this buddy. + /// + uint EntityId { get; } + /// /// Gets the actor associated with this buddy. /// @@ -83,6 +89,9 @@ internal unsafe class BuddyMember : IBuddyMember /// public uint ObjectId => this.Struct->EntityId; + /// + public uint EntityId => this.Struct->EntityId; + /// public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId); diff --git a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs index 4209100f0..829949c12 100644 --- a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs +++ b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs @@ -35,8 +35,14 @@ public interface IGameObject : IEquatable /// /// Gets the data ID for linking to other respective game data. /// + [Obsolete("Renamed to BaseId")] public uint DataId { get; } + /// + /// Gets the base ID for linking to other respective game data. + /// + public uint BaseId { get; } + /// /// Gets the ID of this GameObject's owner. /// @@ -208,6 +214,9 @@ internal unsafe partial class GameObject : IGameObject /// public uint DataId => this.Struct->BaseId; + /// + public uint BaseId => this.Struct->BaseId; + /// public uint OwnerId => this.Struct->OwnerId; diff --git a/Dalamud/Game/ClientState/Party/PartyMember.cs b/Dalamud/Game/ClientState/Party/PartyMember.cs index 65b752808..4c738d866 100644 --- a/Dalamud/Game/ClientState/Party/PartyMember.cs +++ b/Dalamud/Game/ClientState/Party/PartyMember.cs @@ -40,8 +40,14 @@ public interface IPartyMember /// /// Gets the actor ID of this party member. /// + [Obsolete("Renamed to EntityId")] uint ObjectId { get; } + /// + /// Gets the entity ID of this party member. + /// + uint EntityId { get; } + /// /// Gets the actor associated with this buddy. /// @@ -115,87 +121,55 @@ internal unsafe class PartyMember : IPartyMember this.Address = address; } - /// - /// Gets the address of this party member in memory. - /// + /// public IntPtr Address { get; } - /// - /// Gets a list of buffs or debuffs applied to this party member. - /// + /// public StatusList Statuses => new(&this.Struct->StatusManager); - /// - /// Gets the position of the party member. - /// + /// public Vector3 Position => this.Struct->Position; - /// - /// Gets the content ID of the party member. - /// + /// public long ContentId => (long)this.Struct->ContentId; - /// - /// Gets the actor ID of this party member. - /// + /// public uint ObjectId => this.Struct->EntityId; - /// - /// Gets the actor associated with this buddy. - /// - /// - /// This iterates the actor table, it should be used with care. - /// - public IGameObject? GameObject => Service.Get().SearchById(this.ObjectId); + /// + public uint EntityId => this.Struct->EntityId; - /// - /// Gets the current HP of this party member. - /// + /// + public IGameObject? GameObject => Service.Get().SearchById(this.EntityId); + + /// public uint CurrentHP => this.Struct->CurrentHP; - /// - /// Gets the maximum HP of this party member. - /// + /// public uint MaxHP => this.Struct->MaxHP; - /// - /// Gets the current MP of this party member. - /// + /// public ushort CurrentMP => this.Struct->CurrentMP; - /// - /// Gets the maximum MP of this party member. - /// + /// public ushort MaxMP => this.Struct->MaxMP; - /// - /// Gets the territory this party member is located in. - /// + /// public RowRef Territory => LuminaUtils.CreateRef(this.Struct->TerritoryType); - /// - /// Gets the World this party member resides in. - /// + /// public RowRef World => LuminaUtils.CreateRef(this.Struct->HomeWorld); - /// - /// Gets the displayname of this party member. - /// + /// public SeString Name => SeString.Parse(this.Struct->Name); - /// - /// Gets the sex of this party member. - /// + /// public byte Sex => this.Struct->Sex; - /// - /// Gets the classjob of this party member. - /// + /// public RowRef ClassJob => LuminaUtils.CreateRef(this.Struct->ClassJob); - /// - /// Gets the level of this party member. - /// + /// public byte Level => this.Struct->Level; private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs index 07ff2fdce..06dc1b11e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs @@ -40,7 +40,7 @@ internal class BuddyListWidget : IDataWindowWidget } else { - ImGui.Text($"[Companion] {member.Address.ToInt64():X} - {member.ObjectId} - {member.DataID}"); + ImGui.Text($"[Companion] {member.Address.ToInt64():X} - {member.EntityId} - {member.DataID}"); if (this.resolveGameData) { var gameObject = member.GameObject; @@ -64,7 +64,7 @@ internal class BuddyListWidget : IDataWindowWidget } else { - ImGui.Text($"[Pet] {member.Address.ToInt64():X} - {member.ObjectId} - {member.DataID}"); + ImGui.Text($"[Pet] {member.Address.ToInt64():X} - {member.EntityId} - {member.DataID}"); if (this.resolveGameData) { var gameObject = member.GameObject; @@ -91,7 +91,7 @@ internal class BuddyListWidget : IDataWindowWidget for (var i = 0; i < count; i++) { var member = buddyList[i]; - ImGui.Text($"[BattleBuddy] [{i}] {member?.Address.ToInt64():X} - {member?.ObjectId} - {member?.DataID}"); + ImGui.Text($"[BattleBuddy] [{i}] {member?.Address.ToInt64():X} - {member?.EntityId} - {member?.DataID}"); if (this.resolveGameData) { var gameObject = member?.GameObject; diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index a1c2eb6b2..ff06618ab 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -753,7 +753,7 @@ public static partial class Util $"{actor.Address.ToInt64():X}:{actor.GameObjectId:X}[{tag}] - {actor.ObjectKind} - {actor.Name} - X{actor.Position.X} Y{actor.Position.Y} Z{actor.Position.Z} D{actor.YalmDistanceX} R{actor.Rotation} - Target: {actor.TargetObjectId:X}\n"; if (actor is Npc npc) - actorString += $" DataId: {npc.DataId} NameId:{npc.NameId}\n"; + actorString += $" BaseId: {npc.BaseId} NameId:{npc.NameId}\n"; if (actor is ICharacter chara) { @@ -787,7 +787,7 @@ public static partial class Util $"{actor.Address.ToInt64():X}:{actor.GameObjectId:X}[{tag}] - {actor.ObjectKind} - {actor.Name} - X{actor.Position.X} Y{actor.Position.Y} Z{actor.Position.Z} D{actor.YalmDistanceX} R{actor.Rotation} - Target: {actor.TargetObjectId:X}\n"; if (actor is Npc npc) - actorString += $" DataId: {npc.DataId} NameId:{npc.NameId}\n"; + actorString += $" BaseId: {npc.BaseId} NameId:{npc.NameId}\n"; if (actor is Character chara) { From d61a35b81fdd2fa6fa7aae12aa956a1d2c5d4714 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 29 Sep 2025 18:08:25 +0200 Subject: [PATCH 071/477] Update Settings Window (#2400) * Load new localization before firing change event * Update texts in SettingsWindow when locale changes * Localize settings search * Update settings search input - Disable when Credits are scrolling, so Search Results aren't shown instead - Select all on single click, as usual for a search bar * Remove unused IsVisible property * Fix General tab being unselected on language change * Fix search results throwing, oops * Missed using LocRef in EnumSettingsEntry * Set CultureInfo before loading locs * Change it to LazyLoc instead So CheapLoc can export localizations... --- .../Windows/Settings/SettingsEntry.cs | 8 +- .../Internal/Windows/Settings/SettingsTab.cs | 8 +- .../Windows/Settings/SettingsWindow.cs | 220 +++++++++--------- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 10 +- .../Settings/Tabs/SettingsTabAutoUpdate.cs | 6 +- .../Windows/Settings/Tabs/SettingsTabDtr.cs | 12 +- .../Settings/Tabs/SettingsTabExperimental.cs | 50 ++-- .../Settings/Tabs/SettingsTabGeneral.cs | 48 ++-- .../Windows/Settings/Tabs/SettingsTabLook.cs | 88 ++++--- .../Settings/Widgets/ButtonSettingsEntry.cs | 10 +- .../Widgets/DevPluginsSettingsEntry.cs | 16 +- .../Settings/Widgets/EnumSettingsEntry{T}.cs | 21 +- .../Settings/Widgets/GapSettingsEntry.cs | 4 +- .../Settings/Widgets/HintSettingsEntry.cs | 7 +- .../Widgets/LanguageChooserSettingsEntry.cs | 11 +- .../Settings/Widgets/SettingsEntry{T}.cs | 28 ++- .../Widgets/ThirdRepoSettingsEntry.cs | 10 +- Dalamud/Localization.cs | 28 +-- Dalamud/Utility/Internal/LazyLoc.cs | 31 +++ imgui/Dalamud.Bindings.ImGui/ImU8String.cs | 8 +- 20 files changed, 341 insertions(+), 283 deletions(-) create mode 100644 Dalamud/Utility/Internal/LazyLoc.cs diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs index 4cb239da3..3f53be07a 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs @@ -1,14 +1,16 @@ -namespace Dalamud.Interface.Internal.Windows.Settings; +using Dalamud.Utility.Internal; + +namespace Dalamud.Interface.Internal.Windows.Settings; /// /// Basic, drawable settings entry. /// -public abstract class SettingsEntry +internal abstract class SettingsEntry { /// /// Gets or sets the public, searchable name of this settings entry. /// - public string? Name { get; protected set; } + public LazyLoc Name { get; protected set; } /// /// Gets or sets a value indicating whether this entry is valid. diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs index bd4a702f5..e43e3c83a 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs @@ -1,19 +1,19 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Settings; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public abstract class SettingsTab : IDisposable +internal abstract class SettingsTab : IDisposable { public abstract SettingsEntry[] Entries { get; } public abstract string Title { get; } - public bool IsOpen { get; set; } = false; + public abstract SettingsOpenKind Kind { get; } - public virtual bool IsVisible { get; } = true; + public bool IsOpen { get; set; } = false; public virtual void OnOpen() { diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 75cbeb836..581ef3746 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -17,20 +17,19 @@ namespace Dalamud.Interface.Internal.Windows.Settings; /// /// The window that allows for general configuration of Dalamud itself. /// -internal class SettingsWindow : Window +internal sealed class SettingsWindow : Window { private readonly SettingsTab[] tabs; - private string searchInput = string.Empty; private bool isSearchInputPrefilled = false; - private SettingsTab setActiveTab = null!; + private SettingsTab? setActiveTab; /// /// Initializes a new instance of the class. /// public SettingsWindow() - : base(Loc.Localize("DalamudSettingsHeader", "Dalamud Settings") + "###XlSettings2", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar) + : base(Title, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar) { this.Size = new Vector2(740, 550); this.SizeConstraints = new WindowSizeConstraints() @@ -52,6 +51,10 @@ internal class SettingsWindow : Window ]; } + private static string Title => Loc.Localize("DalamudSettingsHeader", "Dalamud Settings") + "###XlSettings2"; + + private SettingsTab? CurrentlyOpenTab => this.tabs.FirstOrDefault(tab => tab.IsOpen); + /// /// Open the settings window to the tab specified by . /// @@ -59,7 +62,7 @@ internal class SettingsWindow : Window public void OpenTo(SettingsOpenKind kind) { this.IsOpen = true; - this.SetOpenTab(kind); + this.setActiveTab = this.tabs.Single(tab => tab.Kind == kind); } /// @@ -83,12 +86,19 @@ internal class SettingsWindow : Window /// public override void OnOpen() { + var localization = Service.Get(); + foreach (var settingsTab in this.tabs) { settingsTab.Load(); } - if (!this.isSearchInputPrefilled) this.searchInput = string.Empty; + if (!this.isSearchInputPrefilled) + { + this.searchInput = string.Empty; + } + + localization.LocalizationChanged += this.OnLocalizationChanged; base.OnOpen(); } @@ -99,6 +109,7 @@ internal class SettingsWindow : Window var configuration = Service.Get(); var interfaceManager = Service.Get(); var fontAtlasFactory = Service.Get(); + var localization = Service.Get(); var scaleChanged = !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale); var rebuildFont = !Equals(fontAtlasFactory.DefaultFontSpec, configuration.DefaultFontSpec); @@ -107,7 +118,7 @@ internal class SettingsWindow : Window ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; if (scaleChanged) { - Service.Get().InvokeGlobalScaleChanged(); + interfaceManager.InvokeGlobalScaleChanged(); } fontAtlasFactory.DefaultFontSpecOverride = null; @@ -115,7 +126,7 @@ internal class SettingsWindow : Window if (rebuildFont) { interfaceManager.RebuildFonts(); - Service.Get().InvokeFontChanged(); + interfaceManager.InvokeFontChanged(); } foreach (var settingsTab in this.tabs) @@ -133,98 +144,29 @@ internal class SettingsWindow : Window this.isSearchInputPrefilled = false; this.searchInput = string.Empty; } + + localization.LocalizationChanged -= this.OnLocalizationChanged; } /// public override void Draw() { + ImGui.SetNextItemWidth(-1); + using (ImRaii.Disabled(this.CurrentlyOpenTab is SettingsTabAbout)) + ImGui.InputTextWithHint("###searchInput"u8, Loc.Localize("DalamudSettingsSearchPlaceholder", "Search for settings..."), ref this.searchInput, 100, ImGuiInputTextFlags.AutoSelectAll); + ImGui.Spacing(); + var windowSize = ImGui.GetWindowSize(); - if (ImGui.BeginTabBar("###settingsTabs"u8)) + using (var tabBar = ImRaii.TabBar("###settingsTabs"u8)) { - if (string.IsNullOrEmpty(this.searchInput)) + if (tabBar) { - foreach (var settingsTab in this.tabs.Where(x => x.IsVisible)) - { - var flags = ImGuiTabItemFlags.NoCloseWithMiddleMouseButton; - if (this.setActiveTab == settingsTab) - { - flags |= ImGuiTabItemFlags.SetSelected; - this.setActiveTab = null; - } - - using var tab = ImRaii.TabItem(settingsTab.Title, flags); - if (tab) - { - if (!settingsTab.IsOpen) - { - settingsTab.IsOpen = true; - settingsTab.OnOpen(); - } - - // Don't add padding for the about tab(credits) - { - using var padding = ImRaii.PushStyle( - ImGuiStyleVar.WindowPadding, - new Vector2(2, 2), - settingsTab is not SettingsTabAbout); - using var borderColor = ImRaii.PushColor( - ImGuiCol.Border, - ImGui.GetColorU32(ImGuiCol.ChildBg)); - using var tabChild = ImRaii.Child( - $"###settings_scrolling_{settingsTab.Title}", - new Vector2(-1, -1), - true); - if (tabChild) - settingsTab.Draw(); - } - - settingsTab.PostDraw(); - } - else if (settingsTab.IsOpen) - { - settingsTab.IsOpen = false; - settingsTab.OnClose(); - } - } + if (string.IsNullOrEmpty(this.searchInput)) + this.DrawTabs(); + else + this.DrawSearchResults(); } - else - { - if (ImGui.BeginTabItem("Search Results"u8)) - { - var any = false; - - foreach (var settingsTab in this.tabs.Where(x => x.IsVisible)) - { - var eligible = settingsTab.Entries.Where(x => !x.Name.IsNullOrEmpty() && x.Name.ToLowerInvariant().Contains(this.searchInput.ToLowerInvariant())).ToArray(); - - if (!eligible.Any()) - continue; - - any = true; - - ImGui.TextColored(ImGuiColors.DalamudGrey, settingsTab.Title); - ImGui.Dummy(new Vector2(5)); - - foreach (var settingsTabEntry in eligible) - { - settingsTabEntry.Draw(); - ImGuiHelpers.ScaledDummy(3); - } - - ImGui.Separator(); - - ImGui.Dummy(new Vector2(10)); - } - - if (!any) - ImGui.TextColored(ImGuiColors.DalamudGrey, "No results found..."u8); - - ImGui.EndTabItem(); - } - } - - ImGui.EndTabBar(); } ImGui.SetCursorPos(windowSize - ImGuiHelpers.ScaledVector2(70)); @@ -256,10 +198,88 @@ internal class SettingsWindow : Window } } } + } - ImGui.SetCursorPos(new Vector2(windowSize.X - 250, ImGui.GetTextLineHeightWithSpacing() + (ImGui.GetStyle().FramePadding.Y * 2))); - ImGui.SetNextItemWidth(240); - ImGui.InputTextWithHint("###searchInput"u8, "Search for settings..."u8, ref this.searchInput, 100); + private void DrawTabs() + { + foreach (var settingsTab in this.tabs) + { + var flags = ImGuiTabItemFlags.NoCloseWithMiddleMouseButton; + + if (this.setActiveTab == settingsTab) + { + flags |= ImGuiTabItemFlags.SetSelected; + this.setActiveTab = null; + } + + using var tab = ImRaii.TabItem(settingsTab.Title, flags); + if (tab) + { + if (!settingsTab.IsOpen) + { + settingsTab.IsOpen = true; + settingsTab.OnOpen(); + } + + // Don't add padding for the About tab (credits) + { + using var padding = ImRaii.PushStyle( + ImGuiStyleVar.WindowPadding, + new Vector2(2, 2), + settingsTab is not SettingsTabAbout); + using var borderColor = ImRaii.PushColor( + ImGuiCol.Border, + ImGui.GetColorU32(ImGuiCol.ChildBg)); + using var tabChild = ImRaii.Child( + $"###settings_scrolling_{settingsTab.Title}", + new Vector2(-1, -1), + true); + if (tabChild) + settingsTab.Draw(); + } + + settingsTab.PostDraw(); + } + else if (settingsTab.IsOpen) + { + settingsTab.IsOpen = false; + settingsTab.OnClose(); + } + } + } + + private void DrawSearchResults() + { + using var tab = ImRaii.TabItem(Loc.Localize("DalamudSettingsSearchResults", "Search Results")); + if (!tab) return; + + var any = false; + + foreach (var settingsTab in this.tabs) + { + var eligible = settingsTab.Entries.Where(x => !x.Name.Key.IsNullOrEmpty() && x.Name.ToString().Contains(this.searchInput, StringComparison.InvariantCultureIgnoreCase)); + + if (!eligible.Any()) + continue; + + any |= true; + + ImGui.TextColored(ImGuiColors.DalamudGrey, settingsTab.Title); + ImGui.Dummy(new Vector2(5)); + + foreach (var settingsTabEntry in eligible) + { + settingsTabEntry.Draw(); + ImGuiHelpers.ScaledDummy(3); + } + + ImGui.Separator(); + + ImGui.Dummy(new Vector2(10)); + } + + if (!any) + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsNoSearchResultsFound", "No results found...")); } private void Save() @@ -301,17 +321,9 @@ internal class SettingsWindow : Window Service.Get().RebuildFonts(); } - private void SetOpenTab(SettingsOpenKind kind) + private void OnLocalizationChanged(string langCode) { - this.setActiveTab = kind switch - { - SettingsOpenKind.General => this.tabs[0], - SettingsOpenKind.LookAndFeel => this.tabs[1], - SettingsOpenKind.AutoUpdates => this.tabs[2], - SettingsOpenKind.ServerInfoBar => this.tabs[3], - SettingsOpenKind.Experimental => this.tabs[4], - SettingsOpenKind.About => this.tabs[5], - _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), - }; + this.WindowName = Title; + this.setActiveTab = this.CurrentlyOpenTab; } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 5b7fc7227..74b9b0fd7 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -20,7 +20,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class SettingsTabAbout : SettingsTab +internal sealed class SettingsTabAbout : SettingsTab { private const float CreditFps = 60.0f; private const string ThankYouText = "Thank you!"; @@ -209,10 +209,12 @@ Contribute at: https://github.com/goatcorp/Dalamud .CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async); } - public override SettingsEntry[] Entries { get; } = { }; - public override string Title => Loc.Localize("DalamudAbout", "About"); + public override SettingsOpenKind Kind => SettingsOpenKind.About; + + public override SettingsEntry[] Entries { get; } = []; + /// public override unsafe void OnOpen() { @@ -287,7 +289,7 @@ Contribute at: https://github.com/goatcorp/Dalamud var windowX = ImGui.GetWindowSize().X; - foreach (var creditsLine in this.creditsText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)) + foreach (var creditsLine in this.creditsText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None)) { var lineLenX = ImGui.CalcTextSize(creditsLine).X; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs index b25cdb10b..6b99c5c24 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; @@ -18,7 +18,7 @@ using Dalamud.Plugin.Internal.Types; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class SettingsTabAutoUpdates : SettingsTab +internal sealed class SettingsTabAutoUpdates : SettingsTab { private AutoUpdateBehavior behavior; private bool updateDisabledPlugins; @@ -31,6 +31,8 @@ public class SettingsTabAutoUpdates : SettingsTab public override string Title => Loc.Localize("DalamudSettingsAutoUpdates", "Auto-Updates"); + public override SettingsOpenKind Kind => SettingsOpenKind.AutoUpdates; + public override void Draw() { ImGui.TextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateHint", diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs index 7cd303587..4b055b35b 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs @@ -14,17 +14,19 @@ using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class SettingsTabDtr : SettingsTab +internal sealed class SettingsTabDtr : SettingsTab { private List? dtrOrder; private List? dtrIgnore; private int dtrSpacing; private bool dtrSwapDirection; - public override SettingsEntry[] Entries { get; } = Array.Empty(); - public override string Title => Loc.Localize("DalamudSettingsServerInfoBar", "Server Info Bar"); + public override SettingsOpenKind Kind => SettingsOpenKind.ServerInfoBar; + + public override SettingsEntry[] Entries { get; } = []; + public override void Draw() { ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarHint", "Plugins can put additional information into your server information bar(where world & time can be seen).\nYou can reorder and disable these here.")); @@ -125,8 +127,8 @@ public class SettingsTabDtr : SettingsTab ImGui.GetIO().MousePos = moveMouseTo[moveMouseToIndex]; } - configuration.DtrOrder = order.Concat(orderLeft).ToList(); - configuration.DtrIgnore = ignore.Concat(ignoreLeft).ToList(); + configuration.DtrOrder = [.. order, .. orderLeft]; + configuration.DtrIgnore = [.. ignore, .. ignoreLeft]; if (isOrderChange) dtrBar.ApplySort(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index d3b741142..4ba5bed94 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -6,11 +6,11 @@ using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.ReShadeHandling; -using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal; using Dalamud.Utility; +using Dalamud.Utility.Internal; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -18,33 +18,28 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; "StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class SettingsTabExperimental : SettingsTab +internal sealed class SettingsTabExperimental : SettingsTab { + public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental"); + + public override SettingsOpenKind Kind => SettingsOpenKind.Experimental; + public override SettingsEntry[] Entries { get; } = [ new SettingsEntry( - Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"), - string.Format( - Loc.Localize( - "DalamudSettingsPluginTestHint", - "Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."), - PluginCategoryManager.Locs.Group_Installed, - PluginInstallerWindow.Locs.PluginContext_TestingOptIn), + LazyLoc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"), + LazyLoc.Localize("DalamudSettingsPluginTestHint", "Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"Installed Plugins\" tab of the plugin installer and select \"Receive plugin testing versions\"."), c => c.DoPluginTest, (v, c) => c.DoPluginTest = v), new HintSettingsEntry( - Loc.Localize( - "DalamudSettingsPluginTestWarning", - "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."), + LazyLoc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."), ImGuiColors.DalamudRed), new GapSettingsEntry(5), new ButtonSettingsEntry( - Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"), - Loc.Localize( - "DalamudSettingsClearHiddenHint", - "Restore plugins you have previously hidden from the plugin installer."), + LazyLoc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"), + LazyLoc.Localize("DalamudSettingsClearHiddenHint", "Restore plugins you have previously hidden from the plugin installer."), () => { Service.Get().HiddenPluginInternalName.Clear(); @@ -56,23 +51,16 @@ public class SettingsTabExperimental : SettingsTab new DevPluginsSettingsEntry(), new SettingsEntry( - Loc.Localize( - "DalamudSettingEnableImGuiAsserts", - "Enable ImGui asserts"), - Loc.Localize( - "DalamudSettingEnableImGuiAssertsHint", + LazyLoc.Localize("DalamudSettingEnableImGuiAsserts", "Enable ImGui asserts"), + LazyLoc.Localize("DalamudSettingEnableImGuiAssertsHint", "If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins. " + "This setting does not persist and will reset when the game restarts.\nUse the setting below to enable it at startup."), c => Service.Get().ShowAsserts, (v, _) => Service.Get().ShowAsserts = v), new SettingsEntry( - Loc.Localize( - "DalamudSettingEnableImGuiAssertsAtStartup", - "Always enable ImGui asserts at startup"), - Loc.Localize( - "DalamudSettingEnableImGuiAssertsAtStartupHint", - "This will enable ImGui asserts every time the game starts."), + LazyLoc.Localize("DalamudSettingEnableImGuiAssertsAtStartup", "Always enable ImGui asserts at startup"), + LazyLoc.Localize("DalamudSettingEnableImGuiAssertsAtStartupHint", "This will enable ImGui asserts every time the game starts."), c => c.ImGuiAssertsEnabledAtStartup ?? false, (v, c) => c.ImGuiAssertsEnabledAtStartup = v), @@ -83,10 +71,8 @@ public class SettingsTabExperimental : SettingsTab new GapSettingsEntry(5, true), new EnumSettingsEntry( - Loc.Localize("DalamudSettingsReShadeHandlingMode", "ReShade handling mode"), - Loc.Localize( - "DalamudSettingsReShadeHandlingModeHint", - "You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."), + LazyLoc.Localize("DalamudSettingsReShadeHandlingMode", "ReShade handling mode"), + LazyLoc.Localize("DalamudSettingsReShadeHandlingModeHint", "You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."), c => c.ReShadeHandlingMode, (v, c) => c.ReShadeHandlingMode = v, fallbackValue: ReShadeHandlingMode.Default, @@ -146,8 +132,6 @@ public class SettingsTabExperimental : SettingsTab */ ]; - public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental"); - public override void Draw() { base.Draw(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs index 5e3648ac6..0daa52630 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs @@ -1,23 +1,29 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using CheapLoc; + using Dalamud.Game.Text; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Utility.Internal; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class SettingsTabGeneral : SettingsTab +internal sealed class SettingsTabGeneral : SettingsTab { + public override string Title => Loc.Localize("DalamudSettingsGeneral", "General"); + + public override SettingsOpenKind Kind => SettingsOpenKind.General; + public override SettingsEntry[] Entries { get; } = - { + [ new LanguageChooserSettingsEntry(), new GapSettingsEntry(5), new EnumSettingsEntry( - Loc.Localize("DalamudSettingsChannel", "Dalamud Chat Channel"), - Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."), + LazyLoc.Localize("DalamudSettingsChannel", "Dalamud Chat Channel"), + LazyLoc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."), c => c.GeneralChatType, (v, c) => c.GeneralChatType = v, warning: v => @@ -33,49 +39,47 @@ public class SettingsTabGeneral : SettingsTab new GapSettingsEntry(5), new SettingsEntry( - Loc.Localize("DalamudSettingsWaitForPluginsOnStartup", "Wait for plugins before game loads"), - Loc.Localize("DalamudSettingsWaitForPluginsOnStartupHint", "Do not let the game load, until plugins are loaded."), + LazyLoc.Localize("DalamudSettingsWaitForPluginsOnStartup", "Wait for plugins before game loads"), + LazyLoc.Localize("DalamudSettingsWaitForPluginsOnStartupHint", "Do not let the game load, until plugins are loaded."), c => c.IsResumeGameAfterPluginLoad, (v, c) => c.IsResumeGameAfterPluginLoad = v), new SettingsEntry( - Loc.Localize("DalamudSettingsFlash", "Flash FFXIV window on duty pop"), - Loc.Localize("DalamudSettingsFlashHint", "Flash the FFXIV window in your task bar when a duty is ready."), + LazyLoc.Localize("DalamudSettingsFlash", "Flash FFXIV window on duty pop"), + LazyLoc.Localize("DalamudSettingsFlashHint", "Flash the FFXIV window in your task bar when a duty is ready."), c => c.DutyFinderTaskbarFlash, (v, c) => c.DutyFinderTaskbarFlash = v), new SettingsEntry( - Loc.Localize("DalamudSettingsDutyFinderMessage", "Chatlog message on duty pop"), - Loc.Localize("DalamudSettingsDutyFinderMessageHint", "Send a message in FFXIV chat when a duty is ready."), + LazyLoc.Localize("DalamudSettingsDutyFinderMessage", "Chatlog message on duty pop"), + LazyLoc.Localize("DalamudSettingsDutyFinderMessageHint", "Send a message in FFXIV chat when a duty is ready."), c => c.DutyFinderChatMessage, (v, c) => c.DutyFinderChatMessage = v), new SettingsEntry( - Loc.Localize("DalamudSettingsPrintDalamudWelcomeMsg", "Display Dalamud's welcome message"), - Loc.Localize("DalamudSettingsPrintDalamudWelcomeMsgHint", "Display Dalamud's welcome message in FFXIV chat when logging in with a character."), + LazyLoc.Localize("DalamudSettingsPrintDalamudWelcomeMsg", "Display Dalamud's welcome message"), + LazyLoc.Localize("DalamudSettingsPrintDalamudWelcomeMsgHint", "Display Dalamud's welcome message in FFXIV chat when logging in with a character."), c => c.PrintDalamudWelcomeMsg, (v, c) => c.PrintDalamudWelcomeMsg = v), new SettingsEntry( - Loc.Localize("DalamudSettingsPrintPluginsWelcomeMsg", "Display loaded plugins in the welcome message"), - Loc.Localize("DalamudSettingsPrintPluginsWelcomeMsgHint", "Display loaded plugins in FFXIV chat when logging in with a character."), + LazyLoc.Localize("DalamudSettingsPrintPluginsWelcomeMsg", "Display loaded plugins in the welcome message"), + LazyLoc.Localize("DalamudSettingsPrintPluginsWelcomeMsgHint", "Display loaded plugins in FFXIV chat when logging in with a character."), c => c.PrintPluginsWelcomeMsg, (v, c) => c.PrintPluginsWelcomeMsg = v), new SettingsEntry( - Loc.Localize("DalamudSettingsSystemMenu", "Dalamud buttons in system menu"), - Loc.Localize("DalamudSettingsSystemMenuMsgHint", "Add buttons for Dalamud plugins and settings to the system menu."), + LazyLoc.Localize("DalamudSettingsSystemMenu", "Dalamud buttons in system menu"), + LazyLoc.Localize("DalamudSettingsSystemMenuMsgHint", "Add buttons for Dalamud plugins and settings to the system menu."), c => c.DoButtonsSystemMenu, (v, c) => c.DoButtonsSystemMenu = v), new GapSettingsEntry(5), new SettingsEntry( - Loc.Localize("DalamudSettingDoMbCollect", "Anonymously upload market board data"), - Loc.Localize("DalamudSettingDoMbCollectHint", "Anonymously provide data about in-game economics to Universalis when browsing the market board. This data can't be tied to you in any way and everyone benefits!"), + LazyLoc.Localize("DalamudSettingDoMbCollect", "Anonymously upload market board data"), + LazyLoc.Localize("DalamudSettingDoMbCollectHint", "Anonymously provide data about in-game economics to Universalis when browsing the market board. This data can't be tied to you in any way and everyone benefits!"), c => c.IsMbCollect, (v, c) => c.IsMbCollect = v), - }; - - public override string Title => Loc.Localize("DalamudSettingsGeneral", "General"); + ]; } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index b6aa11db4..9b2c418b6 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -7,54 +7,58 @@ using CheapLoc; using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Game; -using Dalamud.Game.Text; using Dalamud.Interface.Colors; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ImGuiFontChooserDialog; using Dalamud.Interface.ImGuiNotification.Internal; -using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; +using Dalamud.Utility.Internal; + using Serilog; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class SettingsTabLook : SettingsTab +internal sealed class SettingsTabLook : SettingsTab { private static readonly (string, float)[] GlobalUiScalePresets = - { + [ ("80%##DalamudSettingsGlobalUiScaleReset96", 0.8f), ("100%##DalamudSettingsGlobalUiScaleReset12", 1f), ("117%##DalamudSettingsGlobalUiScaleReset14", 14 / 12f), ("150%##DalamudSettingsGlobalUiScaleReset18", 1.5f), ("200%##DalamudSettingsGlobalUiScaleReset24", 2f), ("300%##DalamudSettingsGlobalUiScaleReset36", 3f), - }; + ]; private float globalUiScale; private IFontSpec defaultFontSpec = null!; + public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel"); + + public override SettingsOpenKind Kind => SettingsOpenKind.LookAndFeel; + public override SettingsEntry[] Entries { get; } = [ new GapSettingsEntry(5, true), new ButtonSettingsEntry( - Loc.Localize("DalamudSettingsOpenStyleEditor", "Open Style Editor"), - Loc.Localize("DalamudSettingsStyleEditorHint", "Modify the look & feel of Dalamud windows."), + LazyLoc.Localize("DalamudSettingsOpenStyleEditor", "Open Style Editor"), + LazyLoc.Localize("DalamudSettingsStyleEditorHint", "Modify the look & feel of Dalamud windows."), () => Service.Get().OpenStyleEditor()), new ButtonSettingsEntry( - Loc.Localize("DalamudSettingsOpenNotificationEditor", "Modify Notification Position"), - Loc.Localize("DalamudSettingsNotificationEditorHint", "Choose where Dalamud notifications appear on the screen."), + LazyLoc.Localize("DalamudSettingsOpenNotificationEditor", "Modify Notification Position"), + LazyLoc.Localize("DalamudSettingsNotificationEditorHint", "Choose where Dalamud notifications appear on the screen."), () => Service.Get().StartPositionChooser()), new SettingsEntry( - Loc.Localize("DalamudSettingsUseDarkMode", "Use Windows immersive/dark mode"), - Loc.Localize("DalamudSettingsUseDarkModeHint", "This will cause the FFXIV window title bar to follow your preferred Windows color settings, and switch to dark mode if enabled."), + LazyLoc.Localize("DalamudSettingsUseDarkMode", "Use Windows immersive/dark mode"), + LazyLoc.Localize("DalamudSettingsUseDarkModeHint", "This will cause the FFXIV window title bar to follow your preferred Windows color settings, and switch to dark mode if enabled."), c => c.WindowIsImmersive, (v, c) => c.WindowIsImmersive = v, b => @@ -72,91 +76,87 @@ public class SettingsTabLook : SettingsTab new GapSettingsEntry(5, true), - new HintSettingsEntry(Loc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below.")), + new HintSettingsEntry(LazyLoc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below.")), new GapSettingsEntry(3), new SettingsEntry( - Loc.Localize("DalamudSettingToggleUiHide", "Hide plugin UI when the game UI is toggled off"), - Loc.Localize("DalamudSettingToggleUiHideHint", "Hide any open windows by plugins when toggling the game overlay."), + LazyLoc.Localize("DalamudSettingToggleUiHide", "Hide plugin UI when the game UI is toggled off"), + LazyLoc.Localize("DalamudSettingToggleUiHideHint", "Hide any open windows by plugins when toggling the game overlay."), c => c.ToggleUiHide, (v, c) => c.ToggleUiHide = v), new SettingsEntry( - Loc.Localize("DalamudSettingToggleUiHideDuringCutscenes", "Hide plugin UI during cutscenes"), - Loc.Localize("DalamudSettingToggleUiHideDuringCutscenesHint", "Hide any open windows by plugins during cutscenes."), + LazyLoc.Localize("DalamudSettingToggleUiHideDuringCutscenes", "Hide plugin UI during cutscenes"), + LazyLoc.Localize("DalamudSettingToggleUiHideDuringCutscenesHint", "Hide any open windows by plugins during cutscenes."), c => c.ToggleUiHideDuringCutscenes, (v, c) => c.ToggleUiHideDuringCutscenes = v), new SettingsEntry( - Loc.Localize("DalamudSettingToggleUiHideDuringGpose", "Hide plugin UI while gpose is active"), - Loc.Localize("DalamudSettingToggleUiHideDuringGposeHint", "Hide any open windows by plugins while gpose is active."), + LazyLoc.Localize("DalamudSettingToggleUiHideDuringGpose", "Hide plugin UI while gpose is active"), + LazyLoc.Localize("DalamudSettingToggleUiHideDuringGposeHint", "Hide any open windows by plugins while gpose is active."), c => c.ToggleUiHideDuringGpose, (v, c) => c.ToggleUiHideDuringGpose = v), new GapSettingsEntry(5, true), new SettingsEntry( - Loc.Localize("DalamudSettingToggleFocusManagement", "Use escape to close Dalamud windows"), - Loc.Localize("DalamudSettingToggleFocusManagementHint", "This will cause Dalamud windows to behave like in-game windows when pressing escape.\nThey will close one after another until all are closed. May not work for all plugins."), + LazyLoc.Localize("DalamudSettingToggleFocusManagement", "Use escape to close Dalamud windows"), + LazyLoc.Localize("DalamudSettingToggleFocusManagementHint", "This will cause Dalamud windows to behave like in-game windows when pressing escape.\nThey will close one after another until all are closed. May not work for all plugins."), c => c.IsFocusManagementEnabled, (v, c) => c.IsFocusManagementEnabled = v), // This is applied every frame in InterfaceManager::CheckViewportState() new SettingsEntry( - Loc.Localize("DalamudSettingToggleViewports", "Enable multi-monitor windows"), - Loc.Localize("DalamudSettingToggleViewportsHint", "This will allow you move plugin windows onto other monitors.\nWill only work in Borderless Window or Windowed mode."), + LazyLoc.Localize("DalamudSettingToggleViewports", "Enable multi-monitor windows"), + LazyLoc.Localize("DalamudSettingToggleViewportsHint", "This will allow you move plugin windows onto other monitors.\nWill only work in Borderless Window or Windowed mode."), c => !c.IsDisableViewport, (v, c) => c.IsDisableViewport = !v), new SettingsEntry( - Loc.Localize("DalamudSettingToggleDocking", "Enable window docking"), - Loc.Localize("DalamudSettingToggleDockingHint", "This will allow you to fuse and tab plugin windows."), + LazyLoc.Localize("DalamudSettingToggleDocking", "Enable window docking"), + LazyLoc.Localize("DalamudSettingToggleDockingHint", "This will allow you to fuse and tab plugin windows."), c => c.IsDocking, (v, c) => c.IsDocking = v), new SettingsEntry( - Loc.Localize( - "DalamudSettingEnablePluginUIAdditionalOptions", - "Add a button to the title bar of plugin windows to open additional options"), - Loc.Localize( - "DalamudSettingEnablePluginUIAdditionalOptionsHint", - "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."), + LazyLoc.Localize("DalamudSettingEnablePluginUIAdditionalOptions", "Add a button to the title bar of plugin windows to open additional options"), + LazyLoc.Localize("DalamudSettingEnablePluginUIAdditionalOptionsHint", "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."), c => c.EnablePluginUiAdditionalOptions, (v, c) => c.EnablePluginUiAdditionalOptions = v), new SettingsEntry( - Loc.Localize("DalamudSettingEnablePluginUISoundEffects", "Enable sound effects for plugin windows"), - Loc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."), + LazyLoc.Localize("DalamudSettingEnablePluginUISoundEffects", "Enable sound effects for plugin windows"), + LazyLoc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."), c => c.EnablePluginUISoundEffects, (v, c) => c.EnablePluginUISoundEffects = v), new SettingsEntry( - Loc.Localize("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"), - Loc.Localize("DalamudSettingToggleGamepadNavigationHint", "This will allow you to toggle between game and plugin navigation via L1+L3.\nToggle the PluginInstaller window via R3 if ImGui navigation is enabled."), + LazyLoc.Localize("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"), + LazyLoc.Localize("DalamudSettingToggleGamepadNavigationHint", "This will allow you to toggle between game and plugin navigation via L1+L3.\nToggle the PluginInstaller window via R3 if ImGui navigation is enabled."), c => c.IsGamepadNavigationEnabled, (v, c) => c.IsGamepadNavigationEnabled = v), new SettingsEntry( - Loc.Localize("DalamudSettingToggleTsm", "Show title screen menu"), - Loc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen.\nDisabling this will also hide the Dalamud version text on the title screen."), + LazyLoc.Localize("DalamudSettingToggleTsm", "Show title screen menu"), + LazyLoc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen.\nDisabling this will also hide the Dalamud version text on the title screen."), c => c.ShowTsm, (v, c) => c.ShowTsm = v), new SettingsEntry( - Loc.Localize("DalamudSettingInstallerOpenDefault", "Open the Plugin Installer to the \"Installed Plugins\" tab by default"), - Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."), + LazyLoc.Localize("DalamudSettingInstallerOpenDefault", "Open the Plugin Installer to the \"Installed Plugins\" tab by default"), + LazyLoc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."), c => c.PluginInstallerOpen == PluginInstallerOpenKind.InstalledPlugins, (v, c) => c.PluginInstallerOpen = v ? PluginInstallerOpenKind.InstalledPlugins : PluginInstallerOpenKind.AllPlugins), new SettingsEntry( - Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"), - Loc.Localize("DalamudSettingReducedMotionHint", "This will suppress certain animations from Dalamud, such as the notification popup."), + LazyLoc.Localize("DalamudSettingReducedMotion", "Reduce motions"), + LazyLoc.Localize("DalamudSettingReducedMotionHint", "This will suppress certain animations from Dalamud, such as the notification popup."), c => c.ReduceMotions ?? false, (v, c) => c.ReduceMotions = v), new SettingsEntry( - Loc.Localize("DalamudSettingImeStateIndicatorOpacity", "IME State Indicator Opacity (CJK only)"), - Loc.Localize("DalamudSettingImeStateIndicatorOpacityHint", "When any of CJK IMEs is in use, the state of IME will be shown with the opacity specified here."), + LazyLoc.Localize("DalamudSettingImeStateIndicatorOpacity", "IME State Indicator Opacity (CJK only)"), + LazyLoc.Localize("DalamudSettingImeStateIndicatorOpacityHint", "When any of CJK IMEs is in use, the state of IME will be shown with the opacity specified here."), c => c.ImeStateIndicatorOpacity, (v, c) => c.ImeStateIndicatorOpacity = v) { @@ -176,8 +176,6 @@ public class SettingsTabLook : SettingsTab } ]; - public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel"); - public override void Draw() { var interfaceManager = Service.Get(); @@ -212,7 +210,7 @@ public class SettingsTabLook : SettingsTab var p = stackalloc byte[len]; Encoding.UTF8.GetBytes(buildingFonts, new(p, len)); ImGui.Text( - new ReadOnlySpan(p, len)[..((len + ((Environment.TickCount / 200) % 3)) - 2)]); + new ReadOnlySpan(p, len)[..(len + (Environment.TickCount / 200 % 3) - 2)]); } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ButtonSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ButtonSettingsEntry.cs index b53411c7c..7d72464ac 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ButtonSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ButtonSettingsEntry.cs @@ -1,18 +1,18 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; -using Dalamud.Interface.Utility; +using Dalamud.Utility.Internal; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class ButtonSettingsEntry : SettingsEntry +internal sealed class ButtonSettingsEntry : SettingsEntry { - private readonly string description; + private readonly LazyLoc description; private readonly Action runs; - public ButtonSettingsEntry(string name, string description, Action runs) + public ButtonSettingsEntry(LazyLoc name, LazyLoc description, Action runs) { this.description = description; this.runs = runs; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs index 2e569b565..dd17a946a 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs @@ -6,6 +6,7 @@ using System.Numerics; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration; using Dalamud.Configuration.Internal; @@ -15,13 +16,14 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; +using Dalamud.Utility.Internal; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class DevPluginsSettingsEntry : SettingsEntry +internal sealed class DevPluginsSettingsEntry : SettingsEntry { - private List devPluginLocations = new(); + private List devPluginLocations = []; private bool devPluginLocationsChanged; private string devPluginTempLocation = string.Empty; private string devPluginLocationAddError = string.Empty; @@ -29,25 +31,25 @@ public class DevPluginsSettingsEntry : SettingsEntry public DevPluginsSettingsEntry() { - this.Name = Loc.Localize("DalamudSettingsDevPluginLocation", "Dev Plugin Locations"); + this.Name = LazyLoc.Localize("DalamudSettingsDevPluginLocation", "Dev Plugin Locations"); } public override void OnClose() { this.devPluginLocations = - Service.Get().DevPluginLoadLocations.Select(x => x.Clone()).ToList(); + [.. Service.Get().DevPluginLoadLocations.Select(x => x.Clone())]; } public override void Load() { this.devPluginLocations = - Service.Get().DevPluginLoadLocations.Select(x => x.Clone()).ToList(); + [.. Service.Get().DevPluginLoadLocations.Select(x => x.Clone())]; this.devPluginLocationsChanged = false; } public override void Save() { - Service.Get().DevPluginLoadLocations = this.devPluginLocations.Select(x => x.Clone()).ToList(); + Service.Get().DevPluginLoadLocations = [.. this.devPluginLocations.Select(x => x.Clone())]; if (this.devPluginLocationsChanged) { @@ -59,7 +61,9 @@ public class DevPluginsSettingsEntry : SettingsEntry public override void Draw() { using var id = ImRaii.PushId("devPluginLocation"u8); + ImGui.Text(this.Name); + if (this.devPluginLocationsChanged) { using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen)) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs index 03123ad95..8fb91940e 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs @@ -1,12 +1,15 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility.Internal; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; @@ -23,8 +26,8 @@ internal sealed class EnumSettingsEntry : SettingsEntry private T valueBacking; public EnumSettingsEntry( - string name, - string description, + LazyLoc name, + LazyLoc description, LoadSettingDelegate load, SaveSettingDelegate save, Action? change = null, @@ -61,7 +64,7 @@ internal sealed class EnumSettingsEntry : SettingsEntry } } - public string Description { get; } + public LazyLoc Description { get; } public Action>? CustomDraw { get; init; } @@ -79,7 +82,10 @@ internal sealed class EnumSettingsEntry : SettingsEntry public override void Draw() { - Debug.Assert(this.Name != null, "this.Name != null"); + var name = this.Name.ToString(); + var description = this.Description.ToString(); + + Debug.Assert(!string.IsNullOrWhiteSpace(name), "Name is empty"); if (this.CustomDraw is not null) { @@ -87,7 +93,7 @@ internal sealed class EnumSettingsEntry : SettingsEntry } else { - ImGui.TextWrapped(this.Name); + ImGui.TextWrapped(name); var idx = this.valueBacking; var values = Enum.GetValues(); @@ -117,13 +123,14 @@ internal sealed class EnumSettingsEntry : SettingsEntry using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) { var desc = this.FriendlyEnumDescriptionGetter(this.valueBacking); + if (!string.IsNullOrWhiteSpace(desc)) { ImGui.TextWrapped(desc); ImGuiHelpers.ScaledDummy(2); } - ImGui.TextWrapped(this.Description); + ImGui.TextWrapped(description); } if (this.CheckValidity != null) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/GapSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/GapSettingsEntry.cs index 88982b825..c220ea684 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/GapSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/GapSettingsEntry.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; @@ -6,7 +6,7 @@ using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public sealed class GapSettingsEntry : SettingsEntry +internal sealed class GapSettingsEntry : SettingsEntry { private readonly float size; private readonly bool hr; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/HintSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/HintSettingsEntry.cs index 1937adc57..7159effdf 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/HintSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/HintSettingsEntry.cs @@ -3,16 +3,17 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; +using Dalamud.Utility.Internal; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class HintSettingsEntry : SettingsEntry +internal sealed class HintSettingsEntry : SettingsEntry { - private readonly string text; + private readonly LazyLoc text; private readonly Vector4 color; - public HintSettingsEntry(string text, Vector4? color = null) + public HintSettingsEntry(LazyLoc text, Vector4? color = null) { this.text = text; this.color = color ?? ImGuiColors.DalamudGrey; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs index 4e328720b..9459413df 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs @@ -3,15 +3,16 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; -using Dalamud.Interface.Utility; +using Dalamud.Utility.Internal; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public sealed class LanguageChooserSettingsEntry : SettingsEntry +internal sealed class LanguageChooserSettingsEntry : SettingsEntry { private readonly string[] languages; private readonly string[] locLanguages; @@ -20,9 +21,9 @@ public sealed class LanguageChooserSettingsEntry : SettingsEntry public LanguageChooserSettingsEntry() { - this.languages = Localization.ApplicableLangCodes.Prepend("en").ToArray(); + this.languages = [.. Localization.ApplicableLangCodes.Prepend("en")]; - this.Name = Loc.Localize("DalamudSettingsLanguage", "Language"); + this.Name = LazyLoc.Localize("DalamudSettingsLanguage", "Language"); this.IsValid = true; this.IsVisible = true; @@ -46,7 +47,7 @@ public sealed class LanguageChooserSettingsEntry : SettingsEntry } } - this.locLanguages = locLanguagesList.ToArray(); + this.locLanguages = [.. locLanguagesList]; } catch (Exception) { diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs index 2349d0d7a..e901550da 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs @@ -1,12 +1,12 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility.Internal; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; @@ -20,8 +20,8 @@ internal sealed class SettingsEntry : SettingsEntry private object? valueBacking; public SettingsEntry( - string name, - string description, + LazyLoc name, + LazyLoc description, LoadSettingDelegate load, SaveSettingDelegate save, Action? change = null, @@ -55,7 +55,7 @@ internal sealed class SettingsEntry : SettingsEntry } } - public string Description { get; } + public LazyLoc Description { get; } public Action>? CustomDraw { get; init; } @@ -69,7 +69,10 @@ internal sealed class SettingsEntry : SettingsEntry public override void Draw() { - Debug.Assert(this.Name != null, "this.Name != null"); + var name = this.Name.ToString(); + var description = this.Description.ToString(); + + Debug.Assert(!string.IsNullOrWhiteSpace(name), "Name is empty"); var type = typeof(T); @@ -79,7 +82,7 @@ internal sealed class SettingsEntry : SettingsEntry } else if (type == typeof(DirectoryInfo)) { - ImGui.TextWrapped(this.Name); + ImGui.TextWrapped(name); var value = this.Value as DirectoryInfo; var nativeBuffer = value?.FullName ?? string.Empty; @@ -91,7 +94,7 @@ internal sealed class SettingsEntry : SettingsEntry } else if (type == typeof(string)) { - ImGui.TextWrapped(this.Name); + ImGui.TextWrapped(name); var nativeBuffer = this.Value as string ?? string.Empty; @@ -104,16 +107,19 @@ internal sealed class SettingsEntry : SettingsEntry { var nativeValue = this.Value as bool? ?? false; - if (ImGui.Checkbox($"{this.Name}###{this.Id.ToString()}", ref nativeValue)) + if (ImGui.Checkbox($"{name}###{this.Id.ToString()}", ref nativeValue)) { this.valueBacking = nativeValue; this.change?.Invoke(this.Value); } } - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) + if (!string.IsNullOrWhiteSpace(description)) { - ImGui.TextWrapped(this.Description); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) + { + ImGui.TextWrapped(this.Description); + } } if (this.CheckValidity != null) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs index cfcf6b3ac..5737b44db 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs @@ -18,9 +18,9 @@ using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -public class ThirdRepoSettingsEntry : SettingsEntry +internal class ThirdRepoSettingsEntry : SettingsEntry { - private List thirdRepoList = new(); + private List thirdRepoList = []; private bool thirdRepoListChanged; private string thirdRepoTempUrl = string.Empty; private string thirdRepoAddError = string.Empty; @@ -34,20 +34,20 @@ public class ThirdRepoSettingsEntry : SettingsEntry public override void OnClose() { this.thirdRepoList = - Service.Get().ThirdRepoList.Select(x => x.Clone()).ToList(); + [.. Service.Get().ThirdRepoList.Select(x => x.Clone())]; } public override void Load() { this.thirdRepoList = - Service.Get().ThirdRepoList.Select(x => x.Clone()).ToList(); + [.. Service.Get().ThirdRepoList.Select(x => x.Clone())]; this.thirdRepoListChanged = false; } public override void Save() { Service.Get().ThirdRepoList = - this.thirdRepoList.Select(x => x.Clone()).ToList(); + [.. this.thirdRepoList.Select(x => x.Clone())]; if (this.thirdRepoListChanged) { diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index 1e72c3075..0a7086e73 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -112,11 +112,14 @@ public class Localization : IServiceType } /// - /// Set up the UI language with "fallbacks"(original English text). + /// Set up the UI language with "fallbacks" (original English text). /// public void SetupWithFallbacks() { this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture; + + Loc.SetupWithFallbacks(this.assembly); + foreach (var d in Delegate.EnumerateInvocationList(this.LocalizationChanged)) { try @@ -128,8 +131,6 @@ public class Localization : IServiceType Log.Error(ex, "Exception during raise of {handler}", d.Method); } } - - Loc.SetupWithFallbacks(this.assembly); } /// @@ -145,6 +146,17 @@ public class Localization : IServiceType } this.DalamudLanguageCultureInfo = GetCultureInfoFromLangCode(langCode); + + try + { + Loc.Setup(this.ReadLocData(langCode), this.assembly); + } + catch (Exception ex) + { + Log.Error(ex, "Could not load loc {0}. Setting up fallbacks.", langCode); + this.SetupWithFallbacks(); + } + foreach (var d in Delegate.EnumerateInvocationList(this.LocalizationChanged)) { try @@ -156,16 +168,6 @@ public class Localization : IServiceType Log.Error(ex, "Exception during raise of {handler}", d.Method); } } - - try - { - Loc.Setup(this.ReadLocData(langCode), this.assembly); - } - catch (Exception ex) - { - Log.Error(ex, "Could not load loc {0}. Setting up fallbacks.", langCode); - this.SetupWithFallbacks(); - } } /// diff --git a/Dalamud/Utility/Internal/LazyLoc.cs b/Dalamud/Utility/Internal/LazyLoc.cs new file mode 100644 index 000000000..08ea6bd95 --- /dev/null +++ b/Dalamud/Utility/Internal/LazyLoc.cs @@ -0,0 +1,31 @@ +using CheapLoc; + +using Dalamud.Bindings.ImGui; + +namespace Dalamud.Utility.Internal; + +/// +/// Represents a lazily localized string, consisting of a localization key and a fallback value. +/// +/// The localization key used to retrieve the localized value. +/// The fallback text to use if the localization key is not found. +internal readonly record struct LazyLoc(string Key, string Fallback) +{ + public static implicit operator ImU8String(LazyLoc locRef) + => new(locRef.ToString()); + + /// + /// Creates a new instance of with the specified localization key and fallback text. + /// + /// The localization key used to retrieve the localized value. + /// The fallback text to use if the localization key is not found. + /// A instance representing the localized value. + public static LazyLoc Localize(string key, string fallback) + => new(key, fallback); + + /// + public override string ToString() + { + return Loc.Localize(this.Key, this.Fallback); + } +} diff --git a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs index 17756ae61..a62152c39 100644 --- a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs +++ b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs @@ -301,7 +301,7 @@ public ref struct ImU8String { var startingPos = this.Length; this.AppendFormatted(value, format); - FixAlignment(startingPos, alignment); + this.FixAlignment(startingPos, alignment); } public void AppendFormatted(ReadOnlySpan value) => this.AppendFormatted(value, null); @@ -322,7 +322,7 @@ public ref struct ImU8String { var startingPos = this.Length; this.AppendFormatted(value, format); - FixAlignment(startingPos, alignment); + this.FixAlignment(startingPos, alignment); } public void AppendFormatted(object? value) => this.AppendFormatted(value!); @@ -358,13 +358,13 @@ public ref struct ImU8String { var startingPos = this.Length; this.AppendFormatted(value, format); - FixAlignment(startingPos, alignment); + this.FixAlignment(startingPos, alignment); } public void Reserve(int length) { if (length >= AllocFreeBufferSize) - IncreaseBuffer(out _, length); + this.IncreaseBuffer(out _, length); } private void FixAlignment(int startingPos, int alignment) From aa3d6c3efe956ff33698a0717c0b2ecca4dc8cec Mon Sep 17 00:00:00 2001 From: srkizer Date: Tue, 30 Sep 2025 01:09:02 +0900 Subject: [PATCH 072/477] Use long running task for plugin ctor (#2406) --- Dalamud/IoC/Internal/ServiceContainer.cs | 28 +++++++----------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 6745155f6..31d16e02e 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -112,27 +112,15 @@ internal class ServiceContainer : IServiceProvider, IServiceType errorStep = "property injection"; await this.InjectProperties(instance, scopedObjects, scope); + // Invoke ctor from a separate thread (LongRunning will spawn a new one) + // so that it does not count towards thread pool active threads cap. + // Plugin ctor can block to wait for Tasks, as we currently do not support asynchronous plugin init. errorStep = "ctor invocation"; - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var thr = new Thread( - () => - { - try - { - ctor.Invoke(instance, resolvedParams); - } - catch (Exception e) - { - tcs.SetException(e); - return; - } - - tcs.SetResult(); - }); - - thr.Start(); - await tcs.Task.ConfigureAwait(false); - thr.Join(); + await Task.Factory.StartNew( + () => ctor.Invoke(instance, resolvedParams), + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default).ConfigureAwait(false); return instance; } From 7813f5d201e10b238b4070c2336debc1c9b947ca Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:09:32 +0200 Subject: [PATCH 073/477] Update ClientStructs (#2413) 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 39ca6265c..ce64fdfe8 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 39ca6265c77a8c9fc36052b79f2daa9ca7af0c69 +Subproject commit ce64fdfe80e387289b46037deb7de6c514ed8592 From 7bf79bdea68e84c44b5002c381b0d4eb7a8a20de Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:13:08 +0200 Subject: [PATCH 074/477] Update ClientStructs (#2417) 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 ce64fdfe8..c25482543 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit ce64fdfe80e387289b46037deb7de6c514ed8592 +Subproject commit c25482543246de695146c55ad7d571deca650919 From 4422622e1e3c0c8e2ec5b0a6f2dc046afbbdc661 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 13:30:34 +0200 Subject: [PATCH 075/477] Add IPlayerState service --- Dalamud/Game/ChatHandlers.cs | 2 +- .../ClientState/Aetherytes/AetheryteList.cs | 9 +- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 6 +- Dalamud/Game/ClientState/ClientState.cs | 14 +- Dalamud/Game/ClientState/Fates/Fate.cs | 10 +- Dalamud/Game/ClientState/Fates/FateTable.cs | 17 +- .../Game/ClientState/Objects/ObjectTable.cs | 15 +- .../ClientState/Objects/Types/GameObject.cs | 12 +- Dalamud/Game/ClientState/Party/PartyList.cs | 12 +- .../Game/ClientState/Statuses/StatusList.cs | 18 +- .../Game/Network/Internal/NetworkHandlers.cs | 30 +- Dalamud/Game/PlayerState/MentorVersion.cs | 27 + Dalamud/Game/PlayerState/PlayerAttribute.cs | 489 ++++++++++++++++++ Dalamud/Game/PlayerState/PlayerState.cs | 205 ++++++++ Dalamud/Game/PlayerState/Sex.cs | 17 + .../Game/Text/Evaluator/SeStringEvaluator.cs | 6 +- .../Windows/Data/Widgets/GaugeWidget.cs | 6 +- .../Windows/Data/Widgets/ObjectTableWidget.cs | 12 +- .../Windows/Data/Widgets/PluginIpcWidget.cs | 8 +- .../Windows/Data/Widgets/TargetWidget.cs | 6 +- .../Steps/SeStringEvaluatorSelfTestStep.cs | 5 +- Dalamud/Plugin/Services/IClientState.cs | 2 + Dalamud/Plugin/Services/IObjectTable.cs | 8 +- Dalamud/Plugin/Services/IPlayerState.cs | 212 ++++++++ Dalamud/Utility/Util.cs | 7 +- 25 files changed, 1043 insertions(+), 112 deletions(-) create mode 100644 Dalamud/Game/PlayerState/MentorVersion.cs create mode 100644 Dalamud/Game/PlayerState/PlayerAttribute.cs create mode 100644 Dalamud/Game/PlayerState/PlayerState.cs create mode 100644 Dalamud/Game/PlayerState/Sex.cs create mode 100644 Dalamud/Plugin/Services/IPlayerState.cs diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index c57dd70b8..8237219bf 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -77,7 +77,7 @@ internal partial class ChatHandlers : IServiceType } // For injections while logged in - if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) + if (clientState.IsLoggedIn && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) this.PrintWelcomeMessage(); #if !DEBUG && false diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index a3d44d423..4a6d011e9 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -22,7 +23,7 @@ namespace Dalamud.Game.ClientState.Aetherytes; internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList { [ServiceManager.ServiceDependency] - private readonly ClientState clientState = Service.Get(); + private readonly ObjectTable objectTable = Service.Get(); private readonly Telepo* telepoInstance = Telepo.Instance(); @@ -37,7 +38,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis { get { - if (this.clientState.LocalPlayer == null) + if (this.objectTable.LocalPlayer == null) return 0; this.Update(); @@ -59,7 +60,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis return null; } - if (this.clientState.LocalPlayer == null) + if (this.objectTable.LocalPlayer == null) return null; return new AetheryteEntry(this.telepoInstance->TeleportList[index]); @@ -69,7 +70,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis private void Update() { // this is very very important as otherwise it crashes - if (this.clientState.LocalPlayer == null) + if (this.objectTable.LocalPlayer == null) return; this.telepoInstance->UpdateAetheryteList(); diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 84cfd24a3..44774a574 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -24,7 +24,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList private const uint InvalidObjectID = 0xE0000000; [ServiceManager.ServiceDependency] - private readonly ClientState clientState = Service.Get(); + private readonly PlayerState.PlayerState playerState = Service.Get(); [ServiceManager.ServiceConstructor] private BuddyList() @@ -105,10 +105,10 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList /// public IBuddyMember? CreateBuddyMemberReference(IntPtr address) { - if (this.clientState.LocalContentId == 0) + if (address == IntPtr.Zero) return null; - if (address == IntPtr.Zero) + if (!this.playerState.IsLoaded) return null; var buddy = new BuddyMember(address); diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index e92af21c3..64be5cc67 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -46,6 +46,12 @@ internal sealed class ClientState : IInternalDisposableService, IClientState [ServiceManager.ServiceDependency] private readonly NetworkHandlers networkHandlers = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly PlayerState.PlayerState playerState = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ObjectTable objectTable = Service.Get(); + private Hook onLogoutHook; private bool initialized; private ushort territoryTypeId; @@ -184,10 +190,10 @@ internal sealed class ClientState : IInternalDisposableService, IClientState } /// - public IPlayerCharacter? LocalPlayer => Service.GetNullable()?[0] as IPlayerCharacter; + public IPlayerCharacter? LocalPlayer => this.objectTable.LocalPlayer; /// - public unsafe ulong LocalContentId => PlayerState.Instance()->ContentId; + public unsafe ulong LocalContentId => this.playerState.ContentId; /// public unsafe bool IsLoggedIn @@ -241,7 +247,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState public bool IsClientIdle(out ConditionFlag blockingFlag) { blockingFlag = 0; - if (this.LocalPlayer is null) return true; + if (this.objectTable.LocalPlayer is null) return true; var condition = Service.GetNullable(); @@ -368,7 +374,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState if (condition == null || gameGui == null || data == null) return; - if (condition.Any() && this.lastConditionNone && this.LocalPlayer != null) + if (condition.Any() && this.lastConditionNone && this.objectTable.LocalPlayer != null) { Log.Debug("Is login"); this.lastConditionNone = false; diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index 504b690c3..5a82ef0c5 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -150,15 +150,11 @@ internal unsafe partial class Fate /// True or false. public static bool IsValid(Fate fate) { - var clientState = Service.GetNullable(); - - if (fate == null || clientState == null) + if (fate == null) return false; - if (clientState.LocalContentId == 0) - return false; - - return true; + var playerState = Service.Get(); + return playerState.IsLoaded == true; } /// diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 1bf557ad5..2266c762d 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -60,15 +60,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable /// public bool IsValid(IFate fate) { - var clientState = Service.GetNullable(); - - if (fate == null || clientState == null) + if (fate == null) return false; - if (clientState.LocalContentId == 0) - return false; - - return true; + var playerState = Service.Get(); + return playerState.IsLoaded == true; } /// @@ -87,12 +83,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable /// public IFate? CreateFateReference(IntPtr offset) { - var clientState = Service.Get(); - - if (clientState.LocalContentId == 0) + if (offset == IntPtr.Zero) return null; - if (offset == IntPtr.Zero) + var playerState = Service.Get(); + if (!playerState.IsLoaded) return null; return new Fate(offset); diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 84c1b5693..0a5e900f0 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -31,16 +31,16 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable { private static int objectTableLength; - private readonly ClientState clientState; + [ServiceManager.ServiceDependency] + private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly CachedEntry[] cachedObjectTable; private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4]; [ServiceManager.ServiceConstructor] - private unsafe ObjectTable(ClientState clientState) + private unsafe ObjectTable() { - this.clientState = clientState; - var nativeObjectTable = CSGameObjectManager.Instance()->Objects.IndexSorted; objectTableLength = nativeObjectTable.Length; @@ -66,6 +66,9 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable /// public int Length => objectTableLength; + /// + public IPlayerCharacter? LocalPlayer => this[0] as IPlayerCharacter; + /// public IEnumerable PlayerObjects => this.GetPlayerObjects(); @@ -142,10 +145,10 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable { ThreadSafety.AssertMainThread(); - if (this.clientState.LocalContentId == 0) + if (address == nint.Zero) return null; - if (address == nint.Zero) + if (!this.playerState.IsLoaded) return null; var obj = (CSGameObject*)address; diff --git a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs index 829949c12..c37b72961 100644 --- a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs +++ b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs @@ -1,9 +1,7 @@ using System.Numerics; -using System.Runtime.CompilerServices; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Memory; namespace Dalamud.Game.ClientState.Objects.Types; @@ -170,15 +168,11 @@ internal partial class GameObject /// True or false. public static bool IsValid(IGameObject? actor) { - var clientState = Service.GetNullable(); - - if (actor is null || clientState == null) + if (actor == null) return false; - if (clientState.LocalContentId == 0) - return false; - - return true; + var playerState = Service.Get(); + return playerState.IsLoaded == true; } /// diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index a016a8211..0a81095c6 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -25,7 +25,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList private const int AllianceLength = 20; [ServiceManager.ServiceDependency] - private readonly ClientState clientState = Service.Get(); + private readonly PlayerState.PlayerState playerState = Service.Get(); [ServiceManager.ServiceConstructor] private PartyList() @@ -91,10 +91,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreatePartyMemberReference(IntPtr address) { - if (this.clientState.LocalContentId == 0) - return null; - - if (address == IntPtr.Zero) + if (address == IntPtr.Zero || !this.playerState.IsLoaded) return null; return new PartyMember(address); @@ -112,10 +109,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreateAllianceMemberReference(IntPtr address) { - if (this.clientState.LocalContentId == 0) - return null; - - if (address == IntPtr.Zero) + if (address == IntPtr.Zero || !this.playerState.IsLoaded) return null; return new PartyMember(address); diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index a38e45ea3..04d0d822c 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -66,15 +66,14 @@ public sealed unsafe partial class StatusList /// The status object containing the requested data. public static StatusList? CreateStatusListReference(IntPtr address) { + if (address == IntPtr.Zero) + return null; + // The use case for CreateStatusListReference and CreateStatusReference to be static is so // fake status lists can be generated. Since they aren't exposed as services, it's either // here or somewhere else. - var clientState = Service.Get(); - - if (clientState.LocalContentId == 0) - return null; - - if (address == IntPtr.Zero) + var playerState = Service.Get(); + if (!playerState.IsLoaded) return null; return new StatusList(address); @@ -87,12 +86,11 @@ public sealed unsafe partial class StatusList /// The status object containing the requested data. public static Status? CreateStatusReference(IntPtr address) { - var clientState = Service.Get(); - - if (clientState.LocalContentId == 0) + if (address == IntPtr.Zero) return null; - if (address == IntPtr.Zero) + var playerState = Service.Get(); + if (!playerState.IsLoaded) return null; return new Status(address); diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 9b85d0ff3..2f9276cc0 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -16,13 +16,12 @@ using Dalamud.Hooking; using Dalamud.Networking.Http; using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; -using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Network; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Info; + using Lumina.Excel.Sheets; + using Serilog; namespace Dalamud.Game.Network.Internal; @@ -269,29 +268,8 @@ internal unsafe class NetworkHandlers : IInternalDisposableService private static (ulong UploaderId, uint WorldId) GetUploaderInfo() { - var agentLobby = AgentLobby.Instance(); - - var uploaderId = agentLobby->LobbyData.ContentId; - if (uploaderId == 0) - { - var playerState = PlayerState.Instance(); - if (playerState->IsLoaded) - { - uploaderId = playerState->ContentId; - } - } - - var worldId = agentLobby->LobbyData.CurrentWorldId; - if (worldId == 0) - { - var localPlayer = Control.GetLocalPlayer(); - if (localPlayer != null) - { - worldId = localPlayer->CurrentWorld; - } - } - - return (uploaderId, worldId); + var playerState = Service.Get(); + return (playerState.ContentId, playerState.CurrentWorld.RowId); } private unsafe nint CfPopDetour(PublicContentDirector.EnterContentInfoPacket* packetData) diff --git a/Dalamud/Game/PlayerState/MentorVersion.cs b/Dalamud/Game/PlayerState/MentorVersion.cs new file mode 100644 index 000000000..701eda112 --- /dev/null +++ b/Dalamud/Game/PlayerState/MentorVersion.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.PlayerState; + +/// +/// Specifies the mentor certification version for a player. +/// +public enum MentorVersion : byte +{ + /// + /// Indicates that the player has never held mentor status in any expansion. + /// + None = 0, + + /// + /// Indicates that the player was last a mentor during the Shadowbringers expansion. + /// + Shadowbringers = 1, + + /// + /// Indicates that the player was last a mentor during the Endwalker expansion. + /// + Endwalker = 2, + + /// + /// Indicates that the player was last a mentor during the Dawntrail expansion. + /// + Dawntrail = 3, +} diff --git a/Dalamud/Game/PlayerState/PlayerAttribute.cs b/Dalamud/Game/PlayerState/PlayerAttribute.cs new file mode 100644 index 000000000..4db8af107 --- /dev/null +++ b/Dalamud/Game/PlayerState/PlayerAttribute.cs @@ -0,0 +1,489 @@ +namespace Dalamud.Game.PlayerState; + +/// +/// Represents a player's attribute. +/// +public enum PlayerAttribute +{ + /// + /// Strength. + /// + /// + /// Affects physical damage dealt by gladiator's arms, marauder's arms, dark knight's arms, gunbreaker's arms, pugilist's arms, lancer's arms, samurai's arms, reaper's arms, thaumaturge's arms, arcanist's arms, red mage's arms, pictomancer's arms, conjurer's arms, astrologian's arms, sage's arms, and blue mage's arms. + /// + Strength = 1, + + /// + /// Dexterity. + /// + /// + /// Affects physical damage dealt by rogue's arms, viper's arms, archer's arms, machinist's arms, and dancer's arms. + /// + Dexterity = 2, + + /// + /// Vitality. + /// + /// + /// Affects maximum HP. + /// + Vitality = 3, + + /// + /// Intelligence. + /// + /// + /// Affects attack magic potency when role is DPS. + /// + Intelligence = 4, + + /// + /// Mind. + /// + /// + /// Affects healing magic potency. Also affects attack magic potency when role is Healer. + /// + Mind = 5, + + /// + /// Piety. + /// + /// + /// Affects MP regeneration. Regeneration rate is determined by piety. Only applicable when in battle and role is Healer. + /// + Piety = 6, + + /// + /// Health Points. + /// + HP = 7, + + /// + /// Mana Points. + /// + MP = 8, + + /// + /// Tactical Points. + /// + TP = 9, + + /// + /// Gathering Point. + /// + GP = 10, + + /// + /// Crafting Points. + /// + CP = 11, + + /// + /// Physical Damage. + /// + PhysicalDamage = 12, + + /// + /// Magic Damage. + /// + MagicDamage = 13, + + /// + /// Delay. + /// + Delay = 14, + + /// + /// Additional Effect. + /// + AdditionalEffect = 15, + + /// + /// Attack Speed. + /// + AttackSpeed = 16, + + /// + /// Block Rate. + /// + BlockRate = 17, + + /// + /// Block Strength. + /// + BlockStrength = 18, + + /// + /// Tenacity. + /// + /// + /// Affects the amount of physical and magic damage dealt and received, as well as HP restored. The higher the value, the more damage dealt, the more HP restored, and the less damage taken. Only applicable when role is Tank. + /// + Tenacity = 19, + + /// + /// Attack Power. + /// + /// + /// Affects amount of damage dealt by physical attacks. The higher the value, the more damage dealt. + /// + AttackPower = 20, + + /// + /// Defense. + /// + /// + /// Affects the amount of damage taken by physical attacks. The higher the value, the less damage taken. + /// + Defense = 21, + + /// + /// Direct Hit Rate. + /// + /// + /// Affects the rate at which your physical and magic attacks land direct hits, dealing slightly more damage than normal hits. The higher the value, the higher the frequency with which your hits will be direct. Higher values will also result in greater damage for actions which guarantee direct hits. + /// + DirectHitRate = 22, + + /// + /// Evasion. + /// + Evasion = 23, + + /// + /// Magic Defense. + /// + /// + /// Affects the amount of damage taken by magic attacks. The higher the value, the less damage taken. + /// + MagicDefense = 24, + + /// + /// Critical Hit Power. + /// + CriticalHitPower = 25, + + /// + /// Critical Hit Resilience. + /// + CriticalHitResilience = 26, + + /// + /// Critical Hit. + /// + /// + /// Affects the amount of physical and magic damage dealt, as well as HP restored. The higher the value, the higher the frequency with which your hits will be critical/higher the potency of critical hits. + /// + CriticalHit = 27, + + /// + /// Critical Hit Evasion. + /// + CriticalHitEvasion = 28, + + /// + /// Slashing Resistance. + /// + /// + /// Decreases damage done by slashing attacks. + /// + SlashingResistance = 29, + + /// + /// Piercing Resistance. + /// + /// + /// Decreases damage done by piercing attacks. + /// + PiercingResistance = 30, + + /// + /// Blunt Resistance. + /// + /// + /// Decreases damage done by blunt attacks. + /// + BluntResistance = 31, + + /// + /// Projectile Resistance. + /// + ProjectileResistance = 32, + + /// + /// Attack Magic Potency. + /// + /// + /// Affects the amount of damage dealt by magic attacks. + /// + AttackMagicPotency = 33, + + /// + /// Healing Magic Potency. + /// + /// + /// Affects the amount of HP restored via healing magic. + /// + HealingMagicPotency = 34, + + /// + /// Enhancement Magic Potency. + /// + EnhancementMagicPotency = 35, + + /// + /// Elemental Bonus. + /// + ElementalBonus = 36, + + /// + /// Fire Resistance. + /// + /// + /// Decreases fire-aspected damage. + /// + FireResistance = 37, + + /// + /// Ice Resistance. + /// + /// + /// Decreases ice-aspected damage. + /// + IceResistance = 38, + + /// + /// Wind Resistance. + /// + /// + /// Decreases wind-aspected damage. + /// + WindResistance = 39, + + /// + /// Earth Resistance. + /// + /// + /// Decreases earth-aspected damage. + /// + EarthResistance = 40, + + /// + /// Lightning Resistance. + /// + /// + /// Decreases lightning-aspected damage. + /// + LightningResistance = 41, + + /// + /// Water Resistance. + /// + /// + /// Decreases water-aspected damage. + /// + WaterResistance = 42, + + /// + /// Magic Resistance. + /// + MagicResistance = 43, + + /// + /// Determination. + /// + /// + /// Affects the amount of damage dealt by both physical and magic attacks, as well as the amount of HP restored by healing spells. + /// + Determination = 44, + + /// + /// Skill Speed. + /// + /// + /// Affects both the casting and recast timers, as well as the damage over time potency for weaponskills and auto-attacks. The higher the value, the shorter the timers/higher the potency. + /// + SkillSpeed = 45, + + /// + /// Spell Speed. + /// + /// + /// Affects both the casting and recast timers for spells. The higher the value, the shorter the timers. Also affects a spell's damage over time or healing over time potency. + /// + SpellSpeed = 46, + + /// + /// Haste. + /// + Haste = 47, + + /// + /// Morale. + /// + /// + /// In PvP, replaces physical and magical defense in determining damage inflicted by other players. Also influences the amount of damage dealt to other players. + /// + Morale = 48, + + /// + /// Enmity. + /// + Enmity = 49, + + /// + /// Enmity Reduction. + /// + EnmityReduction = 50, + + /// + /// Desynthesis Skill Gain. + /// + DesynthesisSkillGain = 51, + + /// + /// EXP Bonus. + /// + EXPBonus = 52, + + /// + /// Regen. + /// + Regen = 53, + + /// + /// Special Attribute. + /// + SpecialAttribute = 54, + + /// + /// Main Attribute. + /// + MainAttribute = 55, + + /// + /// Secondary Attribute. + /// + SecondaryAttribute = 56, + + /// + /// Slow Resistance. + /// + /// + /// Shortens the duration of slow. + /// + SlowResistance = 57, + + /// + /// Petrification Resistance. + /// + PetrificationResistance = 58, + + /// + /// Paralysis Resistance. + /// + ParalysisResistance = 59, + + /// + /// Silence Resistance. + /// + /// + /// Shortens the duration of silence. + /// + SilenceResistance = 60, + + /// + /// Blind Resistance. + /// + /// + /// Shortens the duration of blind. + /// + BlindResistance = 61, + + /// + /// Poison Resistance. + /// + /// + /// Shortens the duration of poison. + /// + PoisonResistance = 62, + + /// + /// Stun Resistance. + /// + /// + /// Shortens the duration of stun. + /// + StunResistance = 63, + + /// + /// Sleep Resistance. + /// + /// + /// Shortens the duration of sleep. + /// + SleepResistance = 64, + + /// + /// Bind Resistance. + /// + /// + /// Shortens the duration of bind. + /// + BindResistance = 65, + + /// + /// Heavy Resistance. + /// + /// + /// Shortens the duration of heavy. + /// + HeavyResistance = 66, + + /// + /// Doom Resistance. + /// + DoomResistance = 67, + + /// + /// Reduced Durability Loss. + /// + ReducedDurabilityLoss = 68, + + /// + /// Increased Spiritbond Gain. + /// + IncreasedSpiritbondGain = 69, + + /// + /// Craftsmanship. + /// + /// + /// Affects the amount of progress achieved in a single synthesis step. + /// + Craftsmanship = 70, + + /// + /// Control. + /// + /// + /// Affects the amount of quality improved in a single synthesis step. + /// + Control = 71, + + /// + /// Gathering. + /// + /// + /// Affects the rate at which items are gathered. + /// + Gathering = 72, + + /// + /// Perception. + /// + /// + /// Affects item yield when gathering as a botanist or miner, and the size of fish when fishing or spearfishing. + /// + Perception = 73, +} diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs new file mode 100644 index 000000000..cebdb0ef8 --- /dev/null +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -0,0 +1,205 @@ +using Dalamud.Data; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +using Lumina.Excel; +using Lumina.Excel.Sheets; + +using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; +using GrandCompany = Lumina.Excel.Sheets.GrandCompany; + +namespace Dalamud.Game.PlayerState; + +/// +/// This class contains the PlayerState wrappers. +/// +[ServiceManager.EarlyLoadedService] +[ResolveVia] +internal unsafe class PlayerState : IPlayerState, IServiceType +{ + /// + public bool IsLoaded => CSPlayerState.Instance()->IsLoaded; + + /// + public string CharacterName => this.IsLoaded ? CSPlayerState.Instance()->CharacterNameString : string.Empty; + + /// + public uint EntityId => this.IsLoaded ? CSPlayerState.Instance()->EntityId : default; + + /// + public ulong ContentId => this.IsLoaded ? CSPlayerState.Instance()->ContentId : default; + + /// + public RowRef CurrentWorld + { + get + { + var agentLobby = AgentLobby.Instance(); + return agentLobby->IsLoggedIn + ? LuminaUtils.CreateRef(agentLobby->LobbyData.CurrentWorldId) + : default; + } + } + + /// + public RowRef HomeWorld + { + get + { + var agentLobby = AgentLobby.Instance(); + return agentLobby->IsLoggedIn + ? LuminaUtils.CreateRef(agentLobby->LobbyData.HomeWorldId) + : default; + } + } + + /// + public Sex Sex => this.IsLoaded ? (Sex)CSPlayerState.Instance()->Sex : default; + + /// + public RowRef Race => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->Race) : default; + + /// + public RowRef Tribe => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->Tribe) : default; + + /// + public RowRef ClassJob => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->CurrentClassJobId) : default; + + /// + public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default; + + /// + public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced; + + /// + public short EffectiveLevel => this.IsLoaded ? (this.IsLevelSynced ? CSPlayerState.Instance()->SyncedLevel : CSPlayerState.Instance()->CurrentLevel) : default; + + /// + public RowRef GuardianDeity => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->GuardianDeity) : default; + + /// + public byte BirthMonth => this.IsLoaded ? CSPlayerState.Instance()->BirthMonth : default; + + /// + public byte BirthDay => this.IsLoaded ? CSPlayerState.Instance()->BirthDay : default; + + /// + public RowRef FirstClass => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->FirstClass) : default; + + /// + public RowRef StartTown => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->StartTown) : default; + + /// + public int BaseStrength => this.IsLoaded ? CSPlayerState.Instance()->BaseStrength : default; + + /// + public int BaseDexterity => this.IsLoaded ? CSPlayerState.Instance()->BaseDexterity : default; + + /// + public int BaseVitality => this.IsLoaded ? CSPlayerState.Instance()->BaseVitality : default; + + /// + public int BaseIntelligence => this.IsLoaded ? CSPlayerState.Instance()->BaseIntelligence : default; + + /// + public int BaseMind => this.IsLoaded ? CSPlayerState.Instance()->BaseMind : default; + + /// + public int BasePiety => this.IsLoaded ? CSPlayerState.Instance()->BasePiety : default; + + /// + public RowRef GrandCompany => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->GrandCompany) : default; + + /// + public RowRef HomeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->HomeAetheryteId) : default; + + /// + public ReadOnlySpan> FavouriteAetherytes + { + get + { + var playerState = CSPlayerState.Instance(); + if (playerState->IsLoaded || playerState->FavouriteAetheryteCount == 0) + return []; + + var count = playerState->FavouriteAetheryteCount; + var array = new RowRef[count]; + + for (var i = 0; i < count; i++) + array[i] = LuminaUtils.CreateRef(playerState->FavouriteAetherytes[i]); + + return array; + } + } + + /// + public RowRef FreeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->FreeAetheryteId) : default; + + /// + public uint BaseRestedExperience => this.IsLoaded ? CSPlayerState.Instance()->BaseRestedExperience : default; + + /// + public short PlayerCommendations => this.IsLoaded ? CSPlayerState.Instance()->PlayerCommendations : default; + + /// + public byte DeliveryLevel => this.IsLoaded ? CSPlayerState.Instance()->DeliveryLevel : default; + + /// + public MentorVersion MentorVersion => this.IsLoaded ? (MentorVersion)CSPlayerState.Instance()->MentorVersion : default; + + /// + public int GetAttribute(PlayerAttribute attribute) => this.IsLoaded ? CSPlayerState.Instance()->Attributes[(int)attribute] : default; + + /// + public byte GetGrandCompanyRank(GrandCompany grandCompany) + { + if (!this.IsLoaded) + return default; + + return grandCompany.RowId switch + { + 1 => CSPlayerState.Instance()->GCRankMaelstrom, + 2 => CSPlayerState.Instance()->GCRankTwinAdders, + 3 => CSPlayerState.Instance()->GCRankImmortalFlames, + _ => default, + }; + } + + /// + public short GetClassJobLevel(ClassJob classJob) + { + if (classJob.ExpArrayIndex == -1) + return default; + + if (!this.IsLoaded) + return default; + + return CSPlayerState.Instance()->ClassJobLevels[classJob.ExpArrayIndex]; + } + + /// + public int GetClassJobExperience(ClassJob classJob) + { + if (classJob.ExpArrayIndex == -1) + return default; + + if (!this.IsLoaded) + return default; + + return CSPlayerState.Instance()->ClassJobExperience[classJob.ExpArrayIndex]; + } + + /// + public float GetDesynthesisLevel(ClassJob classJob) + { + if (classJob.DohDolJobIndex == -1) + return default; + + if (!this.IsLoaded) + return default; + + return CSPlayerState.Instance()->DesynthesisLevels[classJob.DohDolJobIndex] / 100f; + } +} diff --git a/Dalamud/Game/PlayerState/Sex.cs b/Dalamud/Game/PlayerState/Sex.cs new file mode 100644 index 000000000..e6ed6cc78 --- /dev/null +++ b/Dalamud/Game/PlayerState/Sex.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Game.PlayerState; + +/// +/// Represents the sex of a character. +/// +public enum Sex : byte +{ + /// + /// Male sex. + /// + Male = 0, + + /// + /// Female sex. + /// + Female = 1, +} diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 9f898bcca..018742271 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -35,7 +35,6 @@ using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; using AddonSheet = Lumina.Excel.Sheets.Addon; -using PlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; using StatusSheet = Lumina.Excel.Sheets.Status; namespace Dalamud.Game.Text.Evaluator; @@ -68,6 +67,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator [ServiceManager.ServiceDependency] private readonly SheetRedirectResolver sheetRedirectResolver = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly ConcurrentDictionary, string> actStrCache = []; private readonly ConcurrentDictionary, string> objStrCache = []; @@ -564,7 +566,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator return false; // the game uses LocalPlayer here, but using PlayerState seems more safe. - return this.ResolveStringExpression(in context, PlayerState.Instance()->EntityId == entityId ? eTrue : eFalse); + return this.ResolveStringExpression(in context, playerState.EntityId == entityId ? eTrue : eFalse); } private bool TryResolveColor(in SeStringContext context, in ReadOnlySePayloadSpan payload) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs index bf6800a53..7a5a9c89b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs @@ -1,7 +1,7 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.JobGauge; using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -29,10 +29,10 @@ internal class GaugeWidget : IDataWindowWidget /// public void Draw() { - var clientState = Service.Get(); + var objectTable = Service.Get(); var jobGauges = Service.Get(); - var player = clientState.LocalPlayer; + var player = objectTable.LocalPlayer; if (player == null) { ImGui.Text("Player is not present"u8); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index 290c7d9a2..9a2de7261 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -4,6 +4,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Gui; +using Dalamud.Game.PlayerState; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -39,12 +40,13 @@ internal class ObjectTableWidget : IDataWindowWidget var chatGui = Service.Get(); var clientState = Service.Get(); + var playerState = Service.Get(); var gameGui = Service.Get(); var objectTable = Service.Get(); var stateString = string.Empty; - if (clientState.LocalPlayer == null) + if (objectTable.LocalPlayer == null) { ImGui.Text("LocalPlayer null."u8); } @@ -55,10 +57,10 @@ internal class ObjectTableWidget : IDataWindowWidget else { stateString += $"ObjectTableLen: {objectTable.Length}\n"; - stateString += $"LocalPlayerName: {clientState.LocalPlayer.Name}\n"; - stateString += $"CurrentWorldName: {(this.resolveGameData ? clientState.LocalPlayer.CurrentWorld.ValueNullable?.Name : clientState.LocalPlayer.CurrentWorld.RowId.ToString())}\n"; - stateString += $"HomeWorldName: {(this.resolveGameData ? clientState.LocalPlayer.HomeWorld.ValueNullable?.Name : clientState.LocalPlayer.HomeWorld.RowId.ToString())}\n"; - stateString += $"LocalCID: {clientState.LocalContentId:X}\n"; + stateString += $"LocalPlayerName: {playerState.CharacterName}\n"; + stateString += $"CurrentWorldName: {(this.resolveGameData ? playerState.CurrentWorld.ValueNullable?.Name : playerState.CurrentWorld.RowId.ToString())}\n"; + stateString += $"HomeWorldName: {(this.resolveGameData ? playerState.HomeWorld.ValueNullable?.Name : playerState.HomeWorld.RowId.ToString())}\n"; + stateString += $"LocalCID: {playerState.ContentId:X}\n"; stateString += $"LastLinkedItem: {chatGui.LastLinkedItemId}\n"; stateString += $"TerritoryType: {clientState.TerritoryType}\n\n"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs index 6c581604e..0ca754a91 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs @@ -1,10 +1,10 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Game.ClientState; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Internal; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -111,12 +111,12 @@ internal class PluginIpcWidget : IDataWindowWidget if (ImGui.Button("Action GO"u8)) { - this.ipcSubGo.InvokeAction(Service.Get().LocalPlayer); + this.ipcSubGo.InvokeAction(Service.Get().LocalPlayer); } if (ImGui.Button("Func GO"u8)) { - this.callGateResponse = this.ipcSubGo.InvokeFunc(Service.Get().LocalPlayer); + this.callGateResponse = this.ipcSubGo.InvokeFunc(Service.Get().LocalPlayer); } if (!this.callGateResponse.IsNullOrEmpty()) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index 081f3ec96..6caf3286d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.Utility; @@ -33,7 +33,7 @@ internal class TargetWidget : IDataWindowWidget { ImGui.Checkbox("Resolve GameData"u8, ref this.resolveGameData); - var clientState = Service.Get(); + var objectTable = Service.Get(); var targetMgr = Service.Get(); if (targetMgr.Target != null) @@ -80,7 +80,7 @@ internal class TargetWidget : IDataWindowWidget if (ImGui.Button("Clear FT"u8)) targetMgr.FocusTarget = null; - var localPlayer = clientState.LocalPlayer; + var localPlayer = objectTable.LocalPlayer; if (localPlayer != null) { diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs index 9853e31d4..e32b6cd2a 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.SeStringHandling.Payloads; @@ -51,8 +52,8 @@ internal class SeStringEvaluatorSelfTestStep : ISelfTestStep // that it returned the local players name by using its EntityId, // and that it didn't include the world name by checking the HomeWorldId against AgentLobby.Instance()->LobbyData.HomeWorldId. - var clientState = Service.Get(); - var localPlayer = clientState.LocalPlayer; + var objectTable = Service.Get(); + var localPlayer = objectTable.LocalPlayer; if (localPlayer is null) { ImGui.Text("You need to be logged in for this step."u8); diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 0342ea77c..de0c5dad8 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -109,11 +109,13 @@ public interface IClientState /// /// Gets the local player character, if one is present. /// + [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if you need to.")] public IPlayerCharacter? LocalPlayer { get; } /// /// Gets the content ID of the local character. /// + [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}")] public ulong LocalContentId { get; } /// diff --git a/Dalamud/Plugin/Services/IObjectTable.cs b/Dalamud/Plugin/Services/IObjectTable.cs index 4c5305513..36cd72ebe 100644 --- a/Dalamud/Plugin/Services/IObjectTable.cs +++ b/Dalamud/Plugin/Services/IObjectTable.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; namespace Dalamud.Plugin.Services; @@ -19,6 +20,11 @@ public interface IObjectTable : IEnumerable /// public int Length { get; } + /// + /// Gets the local player character, if one is present. + /// + public IPlayerCharacter? LocalPlayer { get; } + /// /// Gets an enumerator for accessing player objects. This will only contain BattleChara objects. /// Does not contain any mounts, minions, or accessories. diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs new file mode 100644 index 000000000..dc507e461 --- /dev/null +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -0,0 +1,212 @@ +using Dalamud.Game.PlayerState; + +using Lumina.Excel; +using Lumina.Excel.Sheets; + +namespace Dalamud.Plugin.Services; + +#pragma warning disable SA1400 // Access modifier should be declared: Interface members are public by default + +/// +/// Interface for determining unlock state of various content in the game. +/// +public interface IPlayerState +{ + /// + /// Gets a value indicating whether the local character is loaded. + /// + /// + /// The actual GameObject will not immediately exist when this changes to true. + /// + bool IsLoaded { get; } + + /// + /// Gets the name of the local character. + /// + string CharacterName { get; } + + /// + /// Gets the entity ID of the local character. + /// + uint EntityId { get; } + + /// + /// Gets the content ID of the local character. + /// + ulong ContentId { get; } + + /// + /// Gets the World row for the local character's current world. + /// + RowRef CurrentWorld { get; } + + /// + /// Gets the World row for the local character's home world. + /// + RowRef HomeWorld { get; } + + /// + /// Gets the sex of the local character. + /// + Sex Sex { get; } + + /// + /// Gets the Race row for the local character. + /// + RowRef Race { get; } + + /// + /// Gets the Tribe row for the local character. + /// + RowRef Tribe { get; } + + /// + /// Gets the ClassJob row for the local character's current class/job. + /// + RowRef ClassJob { get; } + + /// + /// Gets the current class/job's level of the local character. + /// + short Level { get; } + + /// + /// Gets a value indicating whether the local character's level is synced. + /// + bool IsLevelSynced { get; } + + /// + /// Gets the effective level of the local character. + /// + short EffectiveLevel { get; } + + /// + /// Gets the GuardianDeity row for the local character. + /// + RowRef GuardianDeity { get; } + + /// + /// Gets the birth month of the local character. + /// + byte BirthMonth { get; } + + /// + /// Gets the birth day of the local character. + /// + byte BirthDay { get; } + + /// + /// Gets the ClassJob row for the local character's starting class. + /// + RowRef FirstClass { get; } + + /// + /// Gets the Town row for the local character's starting town. + /// + RowRef StartTown { get; } + + /// + /// Gets the base strength of the local character. + /// + int BaseStrength { get; } + + /// + /// Gets the base dexterity of the local character. + /// + int BaseDexterity { get; } + + /// + /// Gets the base vitality of the local character. + /// + int BaseVitality { get; } + + /// + /// Gets the base intelligence of the local character. + /// + int BaseIntelligence { get; } + + /// + /// Gets the base mind of the local character. + /// + int BaseMind { get; } + + /// + /// Gets the piety mind of the local character. + /// + int BasePiety { get; } + + /// + /// Gets the GrandCompany row for the local character's current Grand Company affiliation. + /// + RowRef GrandCompany { get; } + + /// + /// Gets the Aetheryte row for the local character's home aetheryte. + /// + RowRef HomeAetheryte { get; } + + /// + /// Gets a span of Aetheryte rows for the local character's favourite aetherytes. + /// + ReadOnlySpan> FavouriteAetherytes { get; } + + /// + /// Gets the Aetheryte row for the local character's free aetheryte. + /// + RowRef FreeAetheryte { get; } + + /// + /// Gets the amount of received player commendations of the local character. + /// + uint BaseRestedExperience { get; } + + /// + /// Gets the amount of received player commendations of the local character. + /// + short PlayerCommendations { get; } + + /// + /// Gets the Carrier Level of Delivery Moogle Quests of the local character. + /// + byte DeliveryLevel { get; } + + /// + /// Gets the mentor version of the local character. + /// + MentorVersion MentorVersion { get; } + + /// + /// Gets the value of an attribute of the local character. + /// + /// The attribute to check. + /// The value of the specific attribute. + int GetAttribute(PlayerAttribute attribute); + + /// + /// Gets the Grand Company rank of the local character. + /// + /// The Grand Company to check. + /// The Grand Company rank of the local character. + byte GetGrandCompanyRank(GrandCompany grandCompany); + + /// + /// Gets the level of the local character's class/job. + /// + /// The ClassJob row to check. + /// The level of the requested class/job. + short GetClassJobLevel(ClassJob classJob); + + /// + /// Gets the experience of the local character's class/job. + /// + /// The ClassJob row to check. + /// The experience of the requested class/job. + int GetClassJobExperience(ClassJob classJob); + + /// + /// Gets the desynthesis level of the local character's crafter job. + /// + /// The ClassJob row to check. + /// The desynthesis level of the requested crafter job. + float GetDesynthesisLevel(ClassJob classJob); +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index ff06618ab..a3e6e16ed 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -18,18 +18,21 @@ using Dalamud.Game; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Support; + using Lumina.Excel.Sheets; + using Serilog; + using TerraFX.Interop.Windows; + using Windows.Win32.System.Memory; using Windows.Win32.System.Ole; using Windows.Win32.UI.WindowsAndMessaging; -using Dalamud.Interface.Internal; - using FLASHWINFO = Windows.Win32.UI.WindowsAndMessaging.FLASHWINFO; using HWND = Windows.Win32.Foundation.HWND; using MEMORY_BASIC_INFORMATION = Windows.Win32.System.Memory.MEMORY_BASIC_INFORMATION; From 8cac4862494bf2a9a31188310b5cb8dc39a42e50 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 14:36:55 +0200 Subject: [PATCH 076/477] Add PluginInterface attribute to PlayerState --- Dalamud/Game/PlayerState/PlayerState.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs index cebdb0ef8..5e5528eca 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -1,4 +1,5 @@ using Dalamud.Data; +using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -15,9 +16,10 @@ namespace Dalamud.Game.PlayerState; /// /// This class contains the PlayerState wrappers. /// +[PluginInterface] [ServiceManager.EarlyLoadedService] [ResolveVia] -internal unsafe class PlayerState : IPlayerState, IServiceType +internal unsafe class PlayerState : IServiceType, IPlayerState { /// public bool IsLoaded => CSPlayerState.Instance()->IsLoaded; From c2fc04c3a80c93694584b3db9eed40963dc9507a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 14:37:11 +0200 Subject: [PATCH 077/477] Improve wording --- Dalamud/Plugin/Services/IClientState.cs | 2 +- Dalamud/Plugin/Services/IPlayerState.cs | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index de0c5dad8..36bf2e296 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -109,7 +109,7 @@ public interface IClientState /// /// Gets the local player character, if one is present. /// - [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if you need to.")] + [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.")] public IPlayerCharacter? LocalPlayer { get; } /// diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index dc507e461..a119f231b 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -8,15 +8,16 @@ namespace Dalamud.Plugin.Services; #pragma warning disable SA1400 // Access modifier should be declared: Interface members are public by default /// -/// Interface for determining unlock state of various content in the game. +/// Interface for determining the players state. /// public interface IPlayerState { /// - /// Gets a value indicating whether the local character is loaded. + /// Gets a value indicating whether the local players data is loaded. /// /// - /// The actual GameObject will not immediately exist when this changes to true. + /// PlayerState is separate from , + /// and as such the game object might not exist when it's loaded. /// bool IsLoaded { get; } @@ -141,37 +142,37 @@ public interface IPlayerState RowRef GrandCompany { get; } /// - /// Gets the Aetheryte row for the local character's home aetheryte. + /// Gets the Aetheryte row for the local player's home aetheryte. /// RowRef HomeAetheryte { get; } /// - /// Gets a span of Aetheryte rows for the local character's favourite aetherytes. + /// Gets a span of Aetheryte rows for the local player's favourite aetherytes. /// ReadOnlySpan> FavouriteAetherytes { get; } /// - /// Gets the Aetheryte row for the local character's free aetheryte. + /// Gets the Aetheryte row for the local player's free aetheryte. /// RowRef FreeAetheryte { get; } /// - /// Gets the amount of received player commendations of the local character. + /// Gets the amount of received player commendations of the local player. /// uint BaseRestedExperience { get; } /// - /// Gets the amount of received player commendations of the local character. + /// Gets the amount of received player commendations of the local player. /// short PlayerCommendations { get; } /// - /// Gets the Carrier Level of Delivery Moogle Quests of the local character. + /// Gets the Carrier Level of Delivery Moogle Quests of the local player. /// byte DeliveryLevel { get; } /// - /// Gets the mentor version of the local character. + /// Gets the mentor version of the local player. /// MentorVersion MentorVersion { get; } From 153870a053252f19e759301ead3a79fcc6b7a72e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 14:37:22 +0200 Subject: [PATCH 078/477] Add mentor states --- Dalamud/Game/PlayerState/PlayerState.cs | 15 +++++++++++++ Dalamud/Plugin/Services/IPlayerState.cs | 28 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs index 5e5528eca..7af067119 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -151,6 +151,21 @@ internal unsafe class PlayerState : IServiceType, IPlayerState /// public MentorVersion MentorVersion => this.IsLoaded ? (MentorVersion)CSPlayerState.Instance()->MentorVersion : default; + /// + public bool IsMentor => this.IsLoaded && CSPlayerState.Instance()->IsMentor(); + + /// + public bool IsBattleMentor => this.IsLoaded && CSPlayerState.Instance()->IsBattleMentor(); + + /// + public bool IsTradeMentor => this.IsLoaded && CSPlayerState.Instance()->IsTradeMentor(); + + /// + public bool IsNovice => this.IsLoaded && CSPlayerState.Instance()->IsNovice(); + + /// + public bool IsReturner => this.IsLoaded && CSPlayerState.Instance()->IsReturner(); + /// public int GetAttribute(PlayerAttribute attribute) => this.IsLoaded ? CSPlayerState.Instance()->Attributes[(int)attribute] : default; diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index a119f231b..98b0c36da 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -176,6 +176,34 @@ public interface IPlayerState /// MentorVersion MentorVersion { get; } + /// + /// Gets a value indicating whether the local player is any kind of Mentor (Battle or Trade Mentor). + /// + bool IsMentor { get; } + + /// + /// Gets a value indicating whether the local player is a Battle Mentor. + /// + bool IsBattleMentor { get; } + + /// + /// Gets a value indicating whether the local player is a Trade Mentor. + /// + bool IsTradeMentor { get; } + + /// + /// Gets a value indicating whether the local player is a novice (aka. Sprout or New Adventurer). + /// + /// + /// Can be if /nastatus was used to deactivate it. + /// + bool IsNovice { get; } + + /// + /// Gets a value indicating whether the local player is a returner. + /// + bool IsReturner { get; } + /// /// Gets the value of an attribute of the local character. /// From a55c8ca773cb0f82a2df0c0e7f934a1f7efe31a8 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 14:38:50 +0200 Subject: [PATCH 079/477] Fix warning --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 018742271..a4efad488 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -566,7 +566,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator return false; // the game uses LocalPlayer here, but using PlayerState seems more safe. - return this.ResolveStringExpression(in context, playerState.EntityId == entityId ? eTrue : eFalse); + return this.ResolveStringExpression(in context, this.playerState.EntityId == entityId ? eTrue : eFalse); } private bool TryResolveColor(in SeStringContext context, in ReadOnlySePayloadSpan payload) From bcf651b5c17e25f48fbc44d96f5837845fd4ee27 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 6 Oct 2025 01:38:35 +0200 Subject: [PATCH 080/477] Fix FavoriteAetherytes --- Dalamud/Game/PlayerState/PlayerState.cs | 9 ++++++--- Dalamud/Plugin/Services/IPlayerState.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs index 7af067119..06d57133d 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -118,15 +118,18 @@ internal unsafe class PlayerState : IServiceType, IPlayerState public RowRef HomeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->HomeAetheryteId) : default; /// - public ReadOnlySpan> FavouriteAetherytes + public ReadOnlySpan> FavoriteAetherytes { get { var playerState = CSPlayerState.Instance(); - if (playerState->IsLoaded || playerState->FavouriteAetheryteCount == 0) - return []; + if (!playerState->IsLoaded) + return default; var count = playerState->FavouriteAetheryteCount; + if (count == 0) + return default; + var array = new RowRef[count]; for (var i = 0; i < count; i++) diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index 98b0c36da..bf84227ef 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -149,7 +149,7 @@ public interface IPlayerState /// /// Gets a span of Aetheryte rows for the local player's favourite aetherytes. /// - ReadOnlySpan> FavouriteAetherytes { get; } + ReadOnlySpan> FavoriteAetherytes { get; } /// /// Gets the Aetheryte row for the local player's free aetheryte. From 2cf869872d5bbf1045858367ae9c3954ec48397b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 6 Oct 2025 02:08:18 +0200 Subject: [PATCH 081/477] Return IReadOnlyList instead of ReadOnlySpan --- Dalamud/Game/PlayerState/PlayerState.cs | 4 +++- Dalamud/Plugin/Services/IPlayerState.cs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs index 06d57133d..c80166dd5 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + using Dalamud.Data; using Dalamud.IoC; using Dalamud.IoC.Internal; @@ -118,7 +120,7 @@ internal unsafe class PlayerState : IServiceType, IPlayerState public RowRef HomeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->HomeAetheryteId) : default; /// - public ReadOnlySpan> FavoriteAetherytes + public IReadOnlyList> FavoriteAetherytes { get { diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index bf84227ef..1a22f58d6 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + using Dalamud.Game.PlayerState; using Lumina.Excel; @@ -147,9 +149,9 @@ public interface IPlayerState RowRef HomeAetheryte { get; } /// - /// Gets a span of Aetheryte rows for the local player's favourite aetherytes. + /// Gets an array of Aetheryte rows for the local player's favourite aetherytes. /// - ReadOnlySpan> FavoriteAetherytes { get; } + IReadOnlyList> FavoriteAetherytes { get; } /// /// Gets the Aetheryte row for the local player's free aetheryte. From 11adffd7005769730161f5c3f1632fea773753a9 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:33:24 +0200 Subject: [PATCH 082/477] Update Excel Schema (#2423) Co-authored-by: github-actions[bot] --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index fae331428..a37961c45 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit fae3314282b38efc2ce1edf0255d4357d924ced1 +Subproject commit a37961c453463f0f60a96f27aa278ef139841857 From 1073227b8345202d82aa7af0950993d9bbe91766 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:06:43 +0200 Subject: [PATCH 083/477] Update ClientStructs (#2419) 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 c25482543..e279b05e9 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit c25482543246de695146c55ad7d571deca650919 +Subproject commit e279b05e9a6d3a0eff85e7bcaec0fe523358afba From 1ae7de26bf03d12bf8e653f74c2f574f7dcecf03 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:40:44 +0200 Subject: [PATCH 084/477] Fix issue in SigScanner. (#2425) --- Dalamud/Game/SigScanner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index 67f4f563e..c8a371aee 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -326,7 +326,7 @@ public class SigScanner : IDisposable, ISigScanner } /// - public nint[] ScanAllText(string signature) => this.ScanAllText(signature, default).ToArray(); + public nint[] ScanAllText(string signature) => this.ScanAllText(signature, CancellationToken.None).ToArray(); /// public IEnumerable ScanAllText(string signature, CancellationToken cancellationToken) @@ -338,7 +338,7 @@ public class SigScanner : IDisposable, ISigScanner { cancellationToken.ThrowIfCancellationRequested(); - var index = IndexOf(mBase, this.TextSectionSize, needle, mask, badShift); + var index = IndexOf(mBase, this.TextSectionSize - (int)(mBase - this.TextSectionBase), needle, mask, badShift); if (index < 0) break; From 4ac4505d72316ebae6ad7beed7561a5b3ca60d82 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 7 Oct 2025 23:03:29 +0200 Subject: [PATCH 085/477] build: 13.0.0.5 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 59cdc1393..7eee4629c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.4 + 13.0.0.5 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 26d2f764c6265b5fce66d7cd0ea0965c8e1b1380 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 8 Oct 2025 03:05:39 +0200 Subject: [PATCH 086/477] Add BitmapFontIcon.EventTutorial --- Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs b/Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs index 427e7902a..8dca7c4ce 100644 --- a/Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs +++ b/Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs @@ -784,4 +784,9 @@ public enum BitmapFontIcon : uint /// The Cross-World Party Member icon. /// CrossWorldPartyMember = 181, + + /// + /// The Event Tutorial icon. + /// + EventTutorial = 182, } From 9852feaf0862a199a28ee131a863a0d65bd9a6cc Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 8 Oct 2025 03:41:31 +0200 Subject: [PATCH 087/477] Add RentedSeStringBuilder Co-authored-by: Soreepeong <3614868+Soreepeong@users.noreply.github.com> --- Dalamud/Utility/RentedSeStringBuilder.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Dalamud/Utility/RentedSeStringBuilder.cs diff --git a/Dalamud/Utility/RentedSeStringBuilder.cs b/Dalamud/Utility/RentedSeStringBuilder.cs new file mode 100644 index 000000000..93a45d967 --- /dev/null +++ b/Dalamud/Utility/RentedSeStringBuilder.cs @@ -0,0 +1,19 @@ +using Lumina.Text; + +namespace Dalamud.Utility; + +/// +/// Provides a temporarily rented from a shared pool. +/// +public readonly struct RentedSeStringBuilder() : IDisposable +{ + /// + /// Gets the rented value from the shared pool. + /// + public SeStringBuilder Builder { get; } = SeStringBuilder.SharedPool.Get(); + + /// + /// Returns the rented to the shared pool. + /// + public void Dispose() => SeStringBuilder.SharedPool.Return(this.Builder); +} From 4a869bad3f4a2ff5f2a8c739c71234c40fa0cf55 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 8 Oct 2025 03:41:34 +0200 Subject: [PATCH 088/477] Use Luminas SeStringBuilder in DalamudAtkTweaks --- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 30 ++++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 83a2f3525..486af463c 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -1,12 +1,12 @@ using CheapLoc; + using Dalamud.Configuration.Internal; using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; using Dalamud.Interface.Internal; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -185,17 +185,23 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService secondStringEntry->ChangeType(ValueType.String); const int color = 539; - var strPlugins = new SeString().Append(new UIForegroundPayload(color)) - .Append($"{SeIconChar.BoxedLetterD.ToIconString()} ") - .Append(new UIForegroundPayload(0)) - .Append(this.locDalamudPlugins).Encode(); - var strSettings = new SeString().Append(new UIForegroundPayload(color)) - .Append($"{SeIconChar.BoxedLetterD.ToIconString()} ") - .Append(new UIForegroundPayload(0)) - .Append(this.locDalamudSettings).Encode(); - firstStringEntry->SetManagedString(strPlugins); - secondStringEntry->SetManagedString(strSettings); + using var rssb = new RentedSeStringBuilder(); + + firstStringEntry->SetManagedString(rssb.Builder + .PushColorType(color) + .Append($"{SeIconChar.BoxedLetterD.ToIconString()} ") + .PopColorType() + .Append(this.locDalamudPlugins) + .GetViewAsSpan()); + + rssb.Builder.Clear(); + secondStringEntry->SetManagedString(rssb.Builder + .PushColorType(color) + .Append($"{SeIconChar.BoxedLetterD.ToIconString()} ") + .PopColorType() + .Append(this.locDalamudSettings) + .GetViewAsSpan()); // open menu with new size var sizeEntry = &atkValueArgs[4]; From ae777000e29c6d58dc0813b5cb585e45f6344664 Mon Sep 17 00:00:00 2001 From: GrittyFrog Date: Sat, 11 Oct 2025 16:50:29 +1100 Subject: [PATCH 089/477] Plugin-registerable self tests The goal of this change is to let plugins register their own self-tests. We do this through the `ISelfTestRegistry` interface. For a plugin it would look like this: ```csharp [PluginService] public ISelfTestRegistry SelfTestRegistry // Somewhere that gets called by your plugin SelfTestRegistry.RegisterTestSteps([ new MySelfTestStep(), new MyOtherSelfTestStep() ]) ``` Where `MySelfTest` and `MyOtherSelfTest` are instances of the existing `ISelfTestStep` interface. The biggest changes are to `SelfTestWindow` and the introduction of `SelfTestWithResults`. I wanted to make sure test state wasn't lost when changing the dropdown state and I was finding it a bit annoying to work with the Dictionary now that we can't just rely on the index of the item. To fix this I moved all the "test run" state into `SelfTestWithResults`, most of the changes to `SelfTestWindow` are derived from that, other then the addition of the combo box. The documentation for this service is a bit sparse, but I wanted to put it up for review first before I invest a bunch of time making nice documentation. I'm keen to hear if we think this is useful or if any changes are needed. --- .../Windows/SelfTest/SelfTestWindow.cs | 279 ++++++++++-------- .../SelfTest/Steps/ActorTableSelfTestStep.cs | 1 + .../Steps/AddonLifecycleSelfTestStep.cs | 1 + .../Steps/AetheryteListSelfTestStep.cs | 1 + .../SelfTest/Steps/ChatSelfTestStep.cs | 1 + .../SelfTest/Steps/CompletionSelfTestStep.cs | 1 + .../SelfTest/Steps/ConditionSelfTestStep.cs | 1 + .../SelfTest/Steps/ContextMenuSelfTestStep.cs | 1 + .../Windows/SelfTest/Steps/DalamudSelfTest.cs | 51 ++++ .../SelfTest/Steps/DutyStateSelfTestStep.cs | 1 + .../Steps/EnterTerritorySelfTestStep.cs | 1 + .../SelfTest/Steps/FateTableSelfTestStep.cs | 1 + .../FrameworkTaskSchedulerSelfTestStep.cs | 1 + .../SelfTest/Steps/GameConfigSelfTestStep.cs | 1 + .../Steps/GamepadStateSelfTestStep.cs | 2 +- .../Steps/HandledExceptionSelfTestStep.cs | 2 + .../SelfTest/Steps/HoverSelfTestStep.cs | 1 + .../SelfTest/Steps/ItemPayloadSelfTestStep.cs | 1 + .../SelfTest/Steps/KeyStateSelfTestStep.cs | 1 + .../SelfTest/Steps/LoginEventSelfTestStep.cs | 1 + .../SelfTest/Steps/LogoutEventSelfTestStep.cs | 1 + .../SelfTest/Steps/LuminaSelfTestStep.cs | 1 + .../SelfTest/Steps/MarketBoardSelfTestStep.cs | 1 + .../SelfTest/Steps/NamePlateSelfTestStep.cs | 1 + .../Steps/NounProcessorSelfTestStep.cs | 1 + .../SelfTest/Steps/PartyFinderSelfTestStep.cs | 1 + .../Steps/SeStringEvaluatorSelfTestStep.cs | 2 +- .../SheetRedirectResolverSelfTestStep.cs | 1 + .../SelfTest/Steps/TargetSelfTestStep.cs | 1 + .../SelfTest/Steps/ToastSelfTestStep.cs | 1 + .../SelfTest/Steps/WaitFramesSelfTestStep.cs | 2 + Dalamud/Plugin/DalamudPluginInterface.cs | 2 + Dalamud/Plugin/IDalamudPluginInterface.cs | 1 + Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs | 54 ++++ .../SelfTest}/ISelfTestStep.cs | 4 +- .../Plugin/SelfTest/Internal/SelfTestGroup.cs | 28 ++ .../SelfTest/Internal/SelfTestRegistry.cs | 124 ++++++++ .../Internal/SelfTestRegistryPluginScoped.cs | 51 ++++ .../SelfTest/Internal/SelfTestWithResults.cs | 173 +++++++++++ .../SelfTest/SelfTestStepResult.cs | 4 +- Dalamud/Utility/Util.cs | 3 +- 41 files changed, 674 insertions(+), 133 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/Steps/DalamudSelfTest.cs create mode 100644 Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs rename Dalamud/{Interface/Internal/Windows/SelfTest/Steps => Plugin/SelfTest}/ISelfTestStep.cs (85%) create mode 100644 Dalamud/Plugin/SelfTest/Internal/SelfTestGroup.cs create mode 100644 Dalamud/Plugin/SelfTest/Internal/SelfTestRegistry.cs create mode 100644 Dalamud/Plugin/SelfTest/Internal/SelfTestRegistryPluginScoped.cs create mode 100644 Dalamud/Plugin/SelfTest/Internal/SelfTestWithResults.cs rename Dalamud/{Interface/Internal/Windows => Plugin}/SelfTest/SelfTestStepResult.cs (81%) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 86036b2ed..b19b4cf96 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -6,11 +6,13 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; -using Dalamud.Interface.Internal.Windows.SelfTest.Steps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; +using Dalamud.Plugin.SelfTest; +using Dalamud.Plugin.SelfTest.Internal; +using Dalamud.Utility; using Lumina.Excel.Sheets; namespace Dalamud.Interface.Internal.Windows.SelfTest; @@ -22,49 +24,14 @@ internal class SelfTestWindow : Window { private static readonly ModuleLog Log = new("AGING"); - private readonly List steps = - [ - new LoginEventSelfTestStep(), - new WaitFramesSelfTestStep(1000), - new FrameworkTaskSchedulerSelfTestStep(), - new EnterTerritorySelfTestStep(148, "Central Shroud"), - new ItemPayloadSelfTestStep(), - new ContextMenuSelfTestStep(), - new NamePlateSelfTestStep(), - new ActorTableSelfTestStep(), - new FateTableSelfTestStep(), - new AetheryteListSelfTestStep(), - new ConditionSelfTestStep(), - new ToastSelfTestStep(), - new TargetSelfTestStep(), - new KeyStateSelfTestStep(), - new GamepadStateSelfTestStep(), - new ChatSelfTestStep(), - new HoverSelfTestStep(), - new LuminaSelfTestStep(true), - new LuminaSelfTestStep(true), - new LuminaSelfTestStep(true), - new LuminaSelfTestStep(true), - new LuminaSelfTestStep(false), - new AddonLifecycleSelfTestStep(), - new PartyFinderSelfTestStep(), - new HandledExceptionSelfTestStep(), - new DutyStateSelfTestStep(), - new GameConfigSelfTestStep(), - new MarketBoardSelfTestStep(), - new SheetRedirectResolverSelfTestStep(), - new NounProcessorSelfTestStep(), - new SeStringEvaluatorSelfTestStep(), - new CompletionSelfTestStep(), - new LogoutEventSelfTestStep() - ]; + private readonly SelfTestRegistry selfTestRegistry; - private readonly Dictionary testIndexToResult = new(); + private List visibleSteps = new(); private bool selfTestRunning = false; - private int currentStep = 0; - private int scrollToStep = -1; - private DateTimeOffset lastTestStart; + private SelfTestGroup? currentTestGroup = null; + private SelfTestWithResults? currentStep = null; + private SelfTestWithResults? scrollToStep = null; /// /// Initializes a new instance of the class. @@ -72,6 +39,7 @@ internal class SelfTestWindow : Window public SelfTestWindow() : base("Dalamud Self-Test", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { + this.selfTestRegistry = Service.Get(); this.Size = new Vector2(800, 800); this.SizeCondition = ImGuiCond.FirstUseEver; @@ -81,6 +49,55 @@ internal class SelfTestWindow : Window /// public override void Draw() { + // Initialize to first group if not set (first time drawing) + if (this.currentTestGroup == null) + { + this.currentTestGroup = this.selfTestRegistry.SelfTestGroups.FirstOrDefault(); // Should always be "Dalamud" + if (this.currentTestGroup != null) + { + this.SelectTestGroup(this.currentTestGroup); + } + } + + // Update visible steps based on current group + if (this.currentTestGroup != null) + { + this.visibleSteps = this.selfTestRegistry.SelfTests + .Where(test => test.Group == this.currentTestGroup.Name).ToList(); + + // Stop tests if no steps available or if current step is no longer valid + if (this.visibleSteps.Count == 0 || (this.currentStep != null && !this.visibleSteps.Contains(this.currentStep))) + { + this.StopTests(); + } + } + + using (var dropdown = ImRaii.Combo("###SelfTestGroupCombo"u8, this.currentTestGroup?.Name ?? string.Empty)) + { + if (dropdown) + { + foreach (var testGroup in this.selfTestRegistry.SelfTestGroups) + { + if (ImGui.Selectable(testGroup.Name)) + { + this.SelectTestGroup(testGroup); + } + + if (!testGroup.Loaded) + { + ImGui.SameLine(); + this.DrawUnloadedIcon(); + } + } + } + } + + if (this.currentTestGroup?.Loaded == false) + { + ImGui.SameLine(); + this.DrawUnloadedIcon(); + } + if (this.selfTestRunning) { if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop)) @@ -92,13 +109,10 @@ internal class SelfTestWindow : Window if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward)) { - this.testIndexToResult[this.currentStep] = (SelfTestStepResult.NotRan, null); - this.steps[this.currentStep].CleanUp(); - this.currentStep++; + this.currentStep.Reset(); + this.MoveToNextTest(); this.scrollToStep = this.currentStep; - this.lastTestStart = DateTimeOffset.Now; - - if (this.currentStep >= this.steps.Count) + if (this.currentStep == null) { this.StopTests(); } @@ -106,38 +120,50 @@ internal class SelfTestWindow : Window } else { + var canRunTests = this.currentTestGroup?.Loaded == true && this.visibleSteps.Count > 0; + + using var disabled = ImRaii.Disabled(!canRunTests); if (ImGuiComponents.IconButton(FontAwesomeIcon.Play)) { this.selfTestRunning = true; - this.currentStep = 0; + this.currentStep = this.visibleSteps.FirstOrDefault(); this.scrollToStep = this.currentStep; - this.testIndexToResult.Clear(); - this.lastTestStart = DateTimeOffset.Now; + foreach (var test in this.visibleSteps) + { + test.Reset(); + } } } ImGui.SameLine(); - ImGui.Text($"Step: {this.currentStep} / {this.steps.Count}"); + var stepNumber = this.currentStep != null ? this.visibleSteps.IndexOf(this.currentStep) : 0; + ImGui.Text($"Step: {stepNumber} / {this.visibleSteps.Count}"); ImGui.Spacing(); + if (this.currentTestGroup?.Loaded == false) + { + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, $"Plugin '{this.currentTestGroup.Name}' is unloaded. No tests available."); + ImGui.Spacing(); + } + this.DrawResultTable(); ImGui.Spacing(); - if (this.currentStep >= this.steps.Count) + if (this.currentStep == null) { if (this.selfTestRunning) { this.StopTests(); } - if (this.testIndexToResult.Any(x => x.Value.Result == SelfTestStepResult.Fail)) + if (this.visibleSteps.Any(test => test.Result == SelfTestStepResult.Fail)) { ImGui.TextColoredWrapped(ImGuiColors.DalamudRed, "One or more checks failed!"u8); } - else + else if (this.visibleSteps.All(test => test.Result == SelfTestStepResult.Pass)) { ImGui.TextColoredWrapped(ImGuiColors.HealerGreen, "All checks passed!"u8); } @@ -153,30 +179,14 @@ internal class SelfTestWindow : Window using var resultChild = ImRaii.Child("SelfTestResultChild"u8, ImGui.GetContentRegionAvail()); if (!resultChild) return; - var step = this.steps[this.currentStep]; - ImGui.Text($"Current: {step.Name}"); + ImGui.Text($"Current: {this.currentStep.Name}"); ImGuiHelpers.ScaledDummy(10); - SelfTestStepResult result; - try + this.currentStep.DrawAndStep(); + if (this.currentStep.Result != SelfTestStepResult.Waiting) { - result = step.RunStep(); - } - catch (Exception ex) - { - Log.Error(ex, $"Step failed: {step.Name}"); - result = SelfTestStepResult.Fail; - } - - if (result != SelfTestStepResult.Waiting) - { - var duration = DateTimeOffset.Now - this.lastTestStart; - this.testIndexToResult[this.currentStep] = (result, duration); - this.currentStep++; - this.scrollToStep = this.currentStep; - - this.lastTestStart = DateTimeOffset.Now; + this.MoveToNextTest(); } } @@ -202,85 +212,62 @@ internal class SelfTestWindow : Window ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableHeadersRow(); - for (var i = 0; i < this.steps.Count; i++) + foreach (var (step, index) in this.visibleSteps.WithIndex()) { - var step = this.steps[i]; ImGui.TableNextRow(); - if (this.selfTestRunning && this.currentStep == i) + if (this.selfTestRunning && this.currentStep == step) { ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ImGui.GetColorU32(ImGuiCol.TableRowBgAlt)); } ImGui.TableSetColumnIndex(0); ImGui.AlignTextToFramePadding(); - ImGui.Text(i.ToString()); + ImGui.Text(index.ToString()); - if (this.selfTestRunning && this.scrollToStep == i) + if (this.selfTestRunning && this.scrollToStep == step) { ImGui.SetScrollHereY(); - this.scrollToStep = -1; + this.scrollToStep = null; } ImGui.TableSetColumnIndex(1); ImGui.AlignTextToFramePadding(); ImGui.Text(step.Name); - if (this.testIndexToResult.TryGetValue(i, out var result)) + ImGui.TableSetColumnIndex(2); + ImGui.AlignTextToFramePadding(); + switch (step.Result) { - ImGui.TableSetColumnIndex(2); - ImGui.AlignTextToFramePadding(); - - switch (result.Result) - { - case SelfTestStepResult.Pass: - ImGui.TextColored(ImGuiColors.HealerGreen, "PASS"u8); - break; - case SelfTestStepResult.Fail: - ImGui.TextColored(ImGuiColors.DalamudRed, "FAIL"u8); - break; - default: - ImGui.TextColored(ImGuiColors.DalamudGrey, "NR"u8); - break; - } - - ImGui.TableSetColumnIndex(3); - if (result.Duration.HasValue) - { - ImGui.AlignTextToFramePadding(); - ImGui.Text(this.FormatTimeSpan(result.Duration.Value)); - } - } - else - { - ImGui.TableSetColumnIndex(2); - ImGui.AlignTextToFramePadding(); - if (this.selfTestRunning && this.currentStep == i) - { + case SelfTestStepResult.Pass: + ImGui.TextColored(ImGuiColors.HealerGreen, "PASS"u8); + break; + case SelfTestStepResult.Fail: + ImGui.TextColored(ImGuiColors.DalamudRed, "FAIL"u8); + break; + case SelfTestStepResult.Waiting: ImGui.TextColored(ImGuiColors.DalamudGrey, "WAIT"u8); - } - else - { + break; + default: ImGui.TextColored(ImGuiColors.DalamudGrey, "NR"u8); - } + break; + } - ImGui.TableSetColumnIndex(3); + ImGui.TableSetColumnIndex(3); + if (step.Duration.HasValue) + { ImGui.AlignTextToFramePadding(); - if (this.selfTestRunning && this.currentStep == i) - { - ImGui.Text(this.FormatTimeSpan(DateTimeOffset.Now - this.lastTestStart)); - } + ImGui.Text(this.FormatTimeSpan(step.Duration.Value)); } ImGui.TableSetColumnIndex(4); - using var id = ImRaii.PushId($"selfTest{i}"); + using var id = ImRaii.PushId($"selfTest{index}"); if (ImGuiComponents.IconButton(FontAwesomeIcon.FastForward)) { this.StopTests(); - this.testIndexToResult.Remove(i); - this.currentStep = i; + this.currentStep = step; + this.currentStep.Reset(); this.selfTestRunning = true; - this.lastTestStart = DateTimeOffset.Now; } if (ImGui.IsItemHovered()) @@ -293,12 +280,14 @@ internal class SelfTestWindow : Window private void StopTests() { this.selfTestRunning = false; + this.currentStep = null; + this.scrollToStep = null; - foreach (var agingStep in this.steps) + foreach (var agingStep in this.visibleSteps) { try { - agingStep.CleanUp(); + agingStep.Finish(); } catch (Exception ex) { @@ -307,6 +296,46 @@ internal class SelfTestWindow : Window } } + /// + /// Makes the active test group. + /// + /// The test group to make active. + private void SelectTestGroup(SelfTestGroup testGroup) + { + this.currentTestGroup = testGroup; + this.StopTests(); + } + + /// + /// Move `currentTest` to the next test. If there are no tests left, set `currentTest` to null. + /// + private void MoveToNextTest() + { + if (this.currentStep == null) + { + this.currentStep = this.visibleSteps.FirstOrDefault(); + return; + } + + var currentIndex = this.visibleSteps.IndexOf(this.currentStep); + this.currentStep = this.visibleSteps.ElementAtOrDefault(currentIndex + 1); + } + + /// + /// Draws the unloaded plugin icon with tooltip. + /// + private void DrawUnloadedIcon() + { + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextColored(ImGuiColors.DalamudGrey, FontAwesomeIcon.Unlink.ToIconString()); + ImGui.PopFont(); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Plugin is unloaded"); + } + } + private string FormatTimeSpan(TimeSpan ts) { var str = ts.ToString("g", CultureInfo.InvariantCulture); diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ActorTableSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ActorTableSelfTestStep.cs index 0f0e10f10..013333935 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ActorTableSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ActorTableSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; +using Dalamud.Plugin.SelfTest; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AddonLifecycleSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AddonLifecycleSelfTestStep.cs index 9ba3c4f2b..d9c9facc7 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AddonLifecycleSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AddonLifecycleSelfTestStep.cs @@ -3,6 +3,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AetheryteListSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AetheryteListSelfTestStep.cs index 4d4f3fbb1..644c23e88 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AetheryteListSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AetheryteListSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Aetherytes; +using Dalamud.Plugin.SelfTest; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs index c2351cfd2..7a2631fbf 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs @@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs index 463616362..a34b058bd 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Command; using Dalamud.Interface.Utility; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ConditionSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ConditionSelfTestStep.cs index 36e544040..89083da48 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ConditionSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ConditionSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Conditions; +using Dalamud.Plugin.SelfTest; using Serilog; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs index 83003f21f..0fe5b4443 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs @@ -8,6 +8,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Plugin.SelfTest; using Lumina.Excel; using Lumina.Excel.Sheets; using Serilog; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/DalamudSelfTest.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/DalamudSelfTest.cs new file mode 100644 index 000000000..d36aec742 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/DalamudSelfTest.cs @@ -0,0 +1,51 @@ +using Dalamud.Plugin.SelfTest.Internal; +using Lumina.Excel.Sheets; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; + +/// +/// Class handling Dalamud self-test registration. +/// +[ServiceManager.EarlyLoadedService] +internal class DalamudSelfTest : IServiceType +{ + [ServiceManager.ServiceConstructor] + private DalamudSelfTest(SelfTestRegistry registry) + { + registry.RegisterDalamudSelfTestSteps([ + new LoginEventSelfTestStep(), + new WaitFramesSelfTestStep(1000), + new FrameworkTaskSchedulerSelfTestStep(), + new EnterTerritorySelfTestStep(148, "Central Shroud"), + new ItemPayloadSelfTestStep(), + new ContextMenuSelfTestStep(), + new NamePlateSelfTestStep(), + new ActorTableSelfTestStep(), + new FateTableSelfTestStep(), + new AetheryteListSelfTestStep(), + new ConditionSelfTestStep(), + new ToastSelfTestStep(), + new TargetSelfTestStep(), + new KeyStateSelfTestStep(), + new GamepadStateSelfTestStep(), + new ChatSelfTestStep(), + new HoverSelfTestStep(), + new LuminaSelfTestStep(true), + new LuminaSelfTestStep(true), + new LuminaSelfTestStep(true), + new LuminaSelfTestStep(true), + new LuminaSelfTestStep(false), + new AddonLifecycleSelfTestStep(), + new PartyFinderSelfTestStep(), + new HandledExceptionSelfTestStep(), + new DutyStateSelfTestStep(), + new GameConfigSelfTestStep(), + new MarketBoardSelfTestStep(), + new SheetRedirectResolverSelfTestStep(), + new NounProcessorSelfTestStep(), + new SeStringEvaluatorSelfTestStep(), + new CompletionSelfTestStep(), + new LogoutEventSelfTestStep() + ]); + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/DutyStateSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/DutyStateSelfTestStep.cs index 9c40aa345..bf3ffa5c9 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/DutyStateSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/DutyStateSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.DutyState; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/EnterTerritorySelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/EnterTerritorySelfTestStep.cs index ff43d7ae3..4cce1e377 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/EnterTerritorySelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/EnterTerritorySelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FateTableSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FateTableSelfTestStep.cs index ba5f769c2..a413f4932 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FateTableSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FateTableSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Fates; +using Dalamud.Plugin.SelfTest; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FrameworkTaskSchedulerSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FrameworkTaskSchedulerSelfTestStep.cs index d952be419..eb6909fa7 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FrameworkTaskSchedulerSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FrameworkTaskSchedulerSelfTestStep.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Dalamud.Game; +using Dalamud.Plugin.SelfTest; using Dalamud.Utility; using Microsoft.VisualBasic.Logging; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GameConfigSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GameConfigSelfTestStep.cs index 67faec5c9..fa29068c5 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GameConfigSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GameConfigSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Config; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs index 04684d521..d272032e7 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs @@ -2,7 +2,7 @@ using System.Linq; using Dalamud.Game.ClientState.GamePad; using Dalamud.Interface.Utility; - +using Dalamud.Plugin.SelfTest; using Lumina.Text.Payloads; using LSeStringBuilder = Lumina.Text.SeStringBuilder; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/HandledExceptionSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/HandledExceptionSelfTestStep.cs index 9757481e4..c9f7df8a5 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/HandledExceptionSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/HandledExceptionSelfTestStep.cs @@ -1,5 +1,7 @@ using System.Runtime.InteropServices; +using Dalamud.Plugin.SelfTest; + namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; /// diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/HoverSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/HoverSelfTestStep.cs index 8c469d210..e33f16e7f 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/HoverSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/HoverSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ItemPayloadSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ItemPayloadSelfTestStep.cs index 261318d6b..c441796a2 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ItemPayloadSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ItemPayloadSelfTestStep.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Plugin.SelfTest; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/KeyStateSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/KeyStateSelfTestStep.cs index 59ffb33ae..2e936884e 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/KeyStateSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/KeyStateSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Keys; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LoginEventSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LoginEventSelfTestStep.cs index b5a6337ce..97df6bf94 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LoginEventSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LoginEventSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LogoutEventSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LogoutEventSelfTestStep.cs index c8788968e..bc337cf3e 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LogoutEventSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LogoutEventSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs index ff9649a14..741dd71b1 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs @@ -1,4 +1,5 @@ using Dalamud.Data; +using Dalamud.Plugin.SelfTest; using Dalamud.Utility; using Lumina.Excel; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs index a7e9eb681..6a45f343a 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs @@ -5,6 +5,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.MarketBoard; using Dalamud.Game.Network.Structures; using Dalamud.Interface.Utility; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NamePlateSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NamePlateSelfTestStep.cs index 120335c04..9cc6045a6 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NamePlateSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NamePlateSelfTestStep.cs @@ -4,6 +4,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui.NamePlate; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs index 682fe7222..ccb23d395 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs @@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game; using Dalamud.Game.Text.Noun; using Dalamud.Game.Text.Noun.Enums; +using Dalamud.Plugin.SelfTest; using LSheets = Lumina.Excel.Sheets; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/PartyFinderSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/PartyFinderSelfTestStep.cs index d8bdd6d53..052f65a21 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/PartyFinderSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/PartyFinderSelfTestStep.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui.PartyFinder; using Dalamud.Game.Gui.PartyFinder.Types; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs index 9853e31d4..7216e04d2 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs @@ -3,7 +3,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState; using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.SeStringHandling.Payloads; - +using Dalamud.Plugin.SelfTest; using Lumina.Text.ReadOnly; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs index cd2e270db..c285fda46 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using Dalamud.Game; using Dalamud.Game.Text.Evaluator.Internal; +using Dalamud.Plugin.SelfTest; using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI.Misc; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/TargetSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/TargetSelfTestStep.cs index f3a1e2aab..4e5e287a4 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/TargetSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/TargetSelfTestStep.cs @@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ToastSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ToastSelfTestStep.cs index 4c66e7380..793473ecc 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ToastSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ToastSelfTestStep.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui.Toast; +using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/WaitFramesSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/WaitFramesSelfTestStep.cs index 35c64376d..b0cc4f454 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/WaitFramesSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/WaitFramesSelfTestStep.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.SelfTest; + namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; /// diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 541071b63..39a4e7e4b 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -16,6 +16,7 @@ using Dalamud.Game.Text.Sanitizer; using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.PluginInstaller; +using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal; @@ -25,6 +26,7 @@ using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; +using Dalamud.Plugin.Services; using Serilog; diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index e98398a16..b8ab55450 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -15,6 +15,7 @@ using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; +using Dalamud.Plugin.Services; namespace Dalamud.Plugin; diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs new file mode 100644 index 000000000..af3b583c9 --- /dev/null +++ b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + +namespace Dalamud.Plugin.SelfTest; + +/// +/// Interface for registering and unregistering self-test steps from plugins. +/// +/// +/// Registering custom self-test steps for your plugin: +/// +/// [PluginService] +/// public ISelfTestRegistry SelfTestRegistry { get; init; } +/// +/// // In your plugin initialization +/// this.SelfTestRegistry.RegisterTestSteps([ +/// new MyCustomSelfTestStep(), +/// new AnotherSelfTestStep() +/// ]); +/// +/// +/// Creating a custom self-test step: +/// +/// public class MyCustomSelfTestStep : ISelfTestStep +/// { +/// public string Name => "My Custom Test"; +/// +/// public SelfTestStepResult RunStep() +/// { +/// // Your test logic here +/// if (/* test condition passes */) +/// return SelfTestStepResult.Pass; +/// +/// if (/* test condition fails */) +/// return SelfTestStepResult.Fail; +/// +/// // Still waiting for test to complete +/// return SelfTestStepResult.Waiting; +/// } +/// +/// public void CleanUp() +/// { +/// // Clean up any resources used by the test +/// } +/// } +/// +/// +public interface ISelfTestRegistry +{ + /// + /// Registers the self-test steps for this plugin. + /// + /// The test steps to register. + public void RegisterTestSteps(IEnumerable steps); +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ISelfTestStep.cs b/Dalamud/Plugin/SelfTest/ISelfTestStep.cs similarity index 85% rename from Dalamud/Interface/Internal/Windows/SelfTest/Steps/ISelfTestStep.cs rename to Dalamud/Plugin/SelfTest/ISelfTestStep.cs index 529d788ab..2b0de7bf3 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ISelfTestStep.cs +++ b/Dalamud/Plugin/SelfTest/ISelfTestStep.cs @@ -1,9 +1,9 @@ -namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; +namespace Dalamud.Plugin.SelfTest; /// /// Interface for test implementations. /// -internal interface ISelfTestStep +public interface ISelfTestStep { /// /// Gets the name of the test. diff --git a/Dalamud/Plugin/SelfTest/Internal/SelfTestGroup.cs b/Dalamud/Plugin/SelfTest/Internal/SelfTestGroup.cs new file mode 100644 index 000000000..e2bc80f4b --- /dev/null +++ b/Dalamud/Plugin/SelfTest/Internal/SelfTestGroup.cs @@ -0,0 +1,28 @@ +namespace Dalamud.Plugin.SelfTest.Internal; + +/// +/// Represents a self-test group with its loaded/unloaded state. +/// +internal class SelfTestGroup +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the test group. + /// Whether the group is currently loaded. + public SelfTestGroup(string name, bool loaded = true) + { + this.Name = name; + this.Loaded = loaded; + } + + /// + /// Gets the name of the test group. + /// + public string Name { get; } + + /// + /// Gets or sets a value indicating whether this test group is currently loaded. + /// + public bool Loaded { get; set; } +} diff --git a/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistry.cs b/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistry.cs new file mode 100644 index 000000000..aa410b2c4 --- /dev/null +++ b/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistry.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Plugin.SelfTest.Internal; + +/// +/// Registry for self-tests that can be run in the SelfTest window. +/// +[ServiceManager.EarlyLoadedService] +internal class SelfTestRegistry : IServiceType +{ + /// + /// The name of the Dalamud test group. + /// + public const string DalamudTestGroup = "Dalamud"; + + private static readonly ModuleLog Log = new("SelfTestRegistry"); + + private List dalamudSelfTests = new(); + private List pluginSelfTests = new(); + private Dictionary allGroups = new(); + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public SelfTestRegistry() + { + } + + /// + /// Gets all available self test groups. + /// + public IEnumerable SelfTestGroups + { + get + { + // Always return Dalamud group first, then plugin groups + if (this.allGroups.TryGetValue(DalamudTestGroup, out var dalamudGroup)) + { + yield return dalamudGroup; + } + + foreach (var group in this.allGroups.Values) + { + if (group.Name != DalamudTestGroup) + { + yield return group; + } + } + } + } + + /// + /// Gets all self tests from all groups. + /// + public IEnumerable SelfTests => this.dalamudSelfTests.Concat(this.pluginSelfTests); + + /// + /// Registers Dalamud self test steps. + /// + /// The steps to register. + public void RegisterDalamudSelfTestSteps(IEnumerable steps) + { + // Ensure Dalamud group exists and is loaded + if (!this.allGroups.ContainsKey(DalamudTestGroup)) + { + this.allGroups[DalamudTestGroup] = new SelfTestGroup(DalamudTestGroup, loaded: true); + } + else + { + this.allGroups[DalamudTestGroup].Loaded = true; + } + + this.dalamudSelfTests.AddRange(steps.Select(step => SelfTestWithResults.FromDalamudStep(step))); + } + + /// + /// Registers plugin self test steps. + /// + /// The plugin registering the tests. + /// The steps to register. + public void RegisterPluginSelfTestSteps(LocalPlugin plugin, IEnumerable steps) + { + // Ensure plugin group exists and is loaded + if (!this.allGroups.ContainsKey(plugin.InternalName)) + { + this.allGroups[plugin.InternalName] = new SelfTestGroup(plugin.InternalName, loaded: true); + } + else + { + this.allGroups[plugin.InternalName].Loaded = true; + } + + this.pluginSelfTests.AddRange(steps.Select(step => SelfTestWithResults.FromPluginStep(plugin, step))); + } + + /// + /// Unregisters all self test steps for a plugin. + /// + /// The plugin to unregister tests for. + public void UnregisterPluginSelfTestSteps(LocalPlugin plugin) + { + // Clean up existing tests for this plugin + this.pluginSelfTests.ForEach(test => + { + if (test.Plugin == plugin) + { + test.Unload(); + } + }); + + this.pluginSelfTests.RemoveAll(test => test.Plugin == plugin); + + // Mark group as unloaded if it exists + if (this.allGroups.ContainsKey(plugin.InternalName)) + { + this.allGroups[plugin.InternalName].Loaded = false; + } + } +} diff --git a/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistryPluginScoped.cs b/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistryPluginScoped.cs new file mode 100644 index 000000000..18c518879 --- /dev/null +++ b/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistryPluginScoped.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Plugin.SelfTest.Internal; + +/// +/// Plugin-scoped version of SelfTestRegistry. +/// +[PluginInterface] +[ServiceManager.ScopedService] +[ResolveVia] +internal class SelfTestRegistryPluginScoped : ISelfTestRegistry, IInternalDisposableService +{ + [ServiceManager.ServiceDependency] + private readonly SelfTestRegistry selfTestRegistry = Service.Get(); + + private readonly LocalPlugin plugin; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin this service belongs to. + [ServiceManager.ServiceConstructor] + public SelfTestRegistryPluginScoped(LocalPlugin plugin) + { + this.plugin = plugin; + } + + /// + /// Gets the plugin name. + /// + public string PluginName { get; private set; } + + /// + /// Registers test steps for this plugin. + /// + /// The test steps to register. + public void RegisterTestSteps(IEnumerable steps) + { + this.selfTestRegistry.RegisterPluginSelfTestSteps(this.plugin, steps); + } + + /// + void IInternalDisposableService.DisposeService() + { + this.selfTestRegistry.UnregisterPluginSelfTestSteps(this.plugin); + } +} diff --git a/Dalamud/Plugin/SelfTest/Internal/SelfTestWithResults.cs b/Dalamud/Plugin/SelfTest/Internal/SelfTestWithResults.cs new file mode 100644 index 000000000..3c03f66a8 --- /dev/null +++ b/Dalamud/Plugin/SelfTest/Internal/SelfTestWithResults.cs @@ -0,0 +1,173 @@ +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Plugin.SelfTest.Internal; + +/// +/// A self test step with result tracking. +/// +internal class SelfTestWithResults +{ + private static readonly ModuleLog Log = new("SelfTest"); + + /// + /// Initializes a new instance of the class. + /// + /// The plugin providing this test. + /// The test group name. + /// The test step. + public SelfTestWithResults(LocalPlugin plugin, string group, ISelfTestStep step) + { + this.Plugin = plugin; + this.Group = group; + this.Step = step; + } + + /// + /// Gets the test group name. + /// + public string Group { get; private set; } + + /// + /// Gets the plugin that defined these tests. null for Dalamud tests. + /// + public LocalPlugin? Plugin { get; private set; } + + /// + /// Gets the test name. + /// + public string Name { get => this.Step.Name; } + + /// + /// Gets a value indicating whether the test has run and finished. + /// + public bool Finished => this.Result == SelfTestStepResult.Fail || this.Result == SelfTestStepResult.Pass; + + /// + /// Gets a value indicating whether the plugin that provided this test has been unloaded. + /// + public bool Unloaded => this.Step == null; + + /// + /// Gets the most recent result of running this test. + /// + public SelfTestStepResult Result { get; private set; } = SelfTestStepResult.NotRan; + + /// + /// Gets the last time this test was started. + /// + public DateTimeOffset? StartTime { get; private set; } = null; + + /// + /// Gets how long it took (or is taking) for this test to execute. + /// + public TimeSpan? Duration { get; private set; } = null; + + /// + /// Gets or sets the Step that our results are for. + /// + /// If null it means the Plugin that provided this test has been unloaded and we can't use this test anymore. + /// + private ISelfTestStep? Step { get; set; } + + /// + /// Creates a SelfTestWithResults from a Dalamud step. + /// + /// The step to wrap. + /// A new SelfTestWithResults instance. + public static SelfTestWithResults FromDalamudStep(ISelfTestStep step) + { + return new SelfTestWithResults(plugin: null, group: "Dalamud", step: step); + } + + /// + /// Creates a SelfTestWithResults from a plugin step. + /// + /// The plugin providing the step. + /// The step to wrap. + /// A new SelfTestWithResults instance. + public static SelfTestWithResults FromPluginStep(LocalPlugin plugin, ISelfTestStep step) + { + return new SelfTestWithResults(plugin: plugin, group: plugin.InternalName, step: step); + } + + /// + /// Reset the test. + /// + public void Reset() + { + this.Result = SelfTestStepResult.NotRan; + this.StartTime = null; + this.Duration = null; + } + + /// + /// Finish the currently running test and clean up any state. This preserves test run results. + /// + public void Finish() + { + if (this.Step == null) + { + return; + } + + if (this.Result == SelfTestStepResult.NotRan) + { + return; + } + + this.Step.CleanUp(); + } + + /// + /// Steps the state of this Self Test. This should be called every frame that we care about the results of this test. + /// + public void DrawAndStep() + { + // If we've been unloaded then there's nothing to do. + if (this.Step == null) + { + return; + } + + // If we have already finished then there's nothing to do + if (this.Finished) + { + return; + } + + // Otherwise, we assume that calling this functions means we are running the test. + if (this.Result == SelfTestStepResult.NotRan) + { + this.StartTime = DateTimeOffset.Now; + this.Result = SelfTestStepResult.Waiting; + } + + try + { + this.Result = this.Step.RunStep(); + } + catch (Exception ex) + { + Log.Error(ex, $"Step failed: {this.Name} ({this.Group})"); + this.Result = SelfTestStepResult.Fail; + } + + this.Duration = DateTimeOffset.Now - this.StartTime; + + // If we ran and finished we need to clean up + if (this.Finished) + { + this.Finish(); + } + } + + /// + /// Unloads the test and cleans up. + /// + public void Unload() + { + this.Finish(); + this.Step = null; + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestStepResult.cs b/Dalamud/Plugin/SelfTest/SelfTestStepResult.cs similarity index 81% rename from Dalamud/Interface/Internal/Windows/SelfTest/SelfTestStepResult.cs rename to Dalamud/Plugin/SelfTest/SelfTestStepResult.cs index 5abc8e2ed..0eaf77381 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestStepResult.cs +++ b/Dalamud/Plugin/SelfTest/SelfTestStepResult.cs @@ -1,9 +1,9 @@ -namespace Dalamud.Interface.Internal.Windows.SelfTest; +namespace Dalamud.Plugin.SelfTest; /// /// Enum declaring result states of tests. /// -internal enum SelfTestStepResult +public enum SelfTestStepResult { /// /// Test was not ran. diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index ff06618ab..4d578db40 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -18,6 +18,7 @@ using Dalamud.Game; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Support; @@ -28,8 +29,6 @@ using Windows.Win32.System.Memory; using Windows.Win32.System.Ole; using Windows.Win32.UI.WindowsAndMessaging; -using Dalamud.Interface.Internal; - using FLASHWINFO = Windows.Win32.UI.WindowsAndMessaging.FLASHWINFO; using HWND = Windows.Win32.Foundation.HWND; using MEMORY_BASIC_INFORMATION = Windows.Win32.System.Memory.MEMORY_BASIC_INFORMATION; From 2207ad0886019646d3f6420ad57977bbe3b38bf4 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 14 Oct 2025 22:51:17 +0200 Subject: [PATCH 090/477] Bump Reloaded.Memory.Buffers to simplify hook buffer acquisition --- Dalamud/Dalamud.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 7eee4629c..6ee946e8e 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -64,8 +64,8 @@ - - + + From db22c5e11149feb9e0d3bec3b4684e1767a7d5c7 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 14 Oct 2025 22:59:50 +0200 Subject: [PATCH 091/477] Remove status badge from readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bbf9ee1ce..547559d1b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Dalamud [![Actions Status](https://github.com/goatcorp/Dalamud/workflows/Build%20Dalamud/badge.svg)](https://github.com/goatcorp/Dalamud/actions) [![Discord Shield](https://discordapp.com/api/guilds/581875019861328007/widget.png?style=shield)](https://discord.gg/3NMcUV5) +# Dalamud [![Discord Shield](https://discordapp.com/api/guilds/581875019861328007/widget.png?style=shield)](https://discord.gg/3NMcUV5)

Dalamud From 9fb0a79885083f6a6fff22dcdfb47f06c189bc44 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 15 Oct 2025 18:34:54 +0000 Subject: [PATCH 092/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index e279b05e9..bf9759183 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e279b05e9a6d3a0eff85e7bcaec0fe523358afba +Subproject commit bf9759183bc9ef50e5d1ccfafcefdde8585612c7 From 5fd24f4beda185ea3d3a6e8d8737d67838e4a2a3 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 15 Oct 2025 21:24:54 +0200 Subject: [PATCH 093/477] Bump Reloaded.Memory.Buffers, fixes conflict with injector --- Dalamud/Dalamud.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 6ee946e8e..b4cfd0a35 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -64,8 +64,8 @@ - - + + From 168a334756901b533df41ae3e4c55eddae35fad3 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 15 Oct 2025 22:28:13 +0200 Subject: [PATCH 094/477] Injector: remove custom DllImports, replace with CsWin32 --- Dalamud.Injector/Dalamud.Injector.csproj | 1 + Dalamud.Injector/EntryPoint.cs | 60 +- Dalamud.Injector/Injector.cs | 33 +- Dalamud.Injector/NativeFunctions.cs | 956 ----------------------- Dalamud.Injector/NativeMethods.json | 4 + Dalamud.Injector/NativeMethods.txt | 8 + 6 files changed, 71 insertions(+), 991 deletions(-) delete mode 100644 Dalamud.Injector/NativeFunctions.cs create mode 100644 Dalamud.Injector/NativeMethods.json create mode 100644 Dalamud.Injector/NativeMethods.txt diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index c8b648f6d..e4b46ccab 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -55,6 +55,7 @@ + diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 4f876102e..b876aa6ed 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -12,14 +12,13 @@ using System.Text.RegularExpressions; using Dalamud.Common; using Dalamud.Common.Game; using Dalamud.Common.Util; - using Newtonsoft.Json; using Reloaded.Memory.Buffers; using Serilog; using Serilog.Core; using Serilog.Events; - -using static Dalamud.Injector.NativeFunctions; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; namespace Dalamud.Injector { @@ -268,9 +267,9 @@ namespace Dalamud.Injector private static OSPlatform DetectPlatformHeuristic() { - var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); - var wineServerCallPtr = NativeFunctions.GetProcAddress(ntdll, "wine_server_call"); - var wineGetHostVersionPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version"); + var ntdll = Windows.Win32.PInvoke.GetModuleHandle("ntdll.dll"); + var wineServerCallPtr = Windows.Win32.PInvoke.GetProcAddress(ntdll, "wine_server_call"); + var wineGetHostVersionPtr = Windows.Win32.PInvoke.GetProcAddress(ntdll, "wine_get_host_version"); var winePlatform = GetWinePlatform(wineGetHostVersionPtr); var isWine = wineServerCallPtr != nint.Zero; @@ -621,10 +620,13 @@ namespace Dalamud.Injector if (warnManualInjection) { - var result = MessageBoxW(IntPtr.Zero, $"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.", "Dalamud", MessageBoxType.IconWarning | MessageBoxType.OkCancel); + var result = Windows.Win32.PInvoke.MessageBox( + HWND.Null, + $"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.", + "Dalamud", + MESSAGEBOX_STYLE.MB_ICONWARNING | MESSAGEBOX_STYLE.MB_OKCANCEL); - // IDCANCEL - if (result == 2) + if (result == MESSAGEBOX_RESULT.IDCANCEL) { Log.Information("User cancelled injection"); return -2; @@ -934,30 +936,48 @@ namespace Dalamud.Injector Inject(process, startInfo, false); } - var processHandleForOwner = IntPtr.Zero; + var processHandleForOwner = HANDLE.Null; if (handleOwner != IntPtr.Zero) { - if (!DuplicateHandle(Process.GetCurrentProcess().Handle, process.Handle, handleOwner, out processHandleForOwner, 0, false, DuplicateOptions.SameAccess)) + unsafe { - Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error()); + if (!Windows.Win32.PInvoke.DuplicateHandle( + new HANDLE(Process.GetCurrentProcess().Handle.ToPointer()), + new HANDLE(process.Handle.ToPointer()), + new HANDLE(handleOwner), + &processHandleForOwner, + 0, + false, + DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS)) + { + Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error()); + } } } - Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {processHandleForOwner}}}"); + Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {(IntPtr)processHandleForOwner}}}"); Log.CloseAndFlush(); return 0; } - private static Process GetInheritableCurrentProcessHandle() + private static unsafe Process GetInheritableCurrentProcessHandle() { - if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess)) + var currentProcessHandle = new HANDLE(Process.GetCurrentProcess().Handle.ToPointer()); + var inheritableHandle = HANDLE.Null; + if (!Windows.Win32.PInvoke.DuplicateHandle( + currentProcessHandle, + currentProcessHandle, + currentProcessHandle, + &inheritableHandle, + 0, + true, + DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS)) { - Log.Error("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error()); - return null; + throw new Win32Exception("Failed to call DuplicateHandle"); } - return new ExistingProcess(inheritableCurrentProcessHandle); + return new ExistingProcess(inheritableHandle); } private static int ProcessLaunchTestCommand(List args) @@ -1048,13 +1068,13 @@ namespace Dalamud.Injector } injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress); - injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode); + var exitCode = injector.CallRemoteFunction(initAddress, startInfoAddress); // ====================================================== if (exitCode > 0) { - Log.Error($"Dalamud.Boot::Initialize returned {exitCode}"); + Log.Error("Dalamud.Boot::Initialize returned {ExitCode}", exitCode); return; } diff --git a/Dalamud.Injector/Injector.cs b/Dalamud.Injector/Injector.cs index 17ca5ccb5..0b57e7cbd 100644 --- a/Dalamud.Injector/Injector.cs +++ b/Dalamud.Injector/Injector.cs @@ -13,8 +13,8 @@ using Reloaded.Memory.Buffers; using Reloaded.Memory.Sources; using Reloaded.Memory.Utilities; using Serilog; +using Windows.Win32.Foundation; -using static Dalamud.Injector.NativeFunctions; using static Iced.Intel.AssemblerRegisters; namespace Dalamud.Injector @@ -88,7 +88,7 @@ namespace Dalamud.Injector if (lpParameter == 0) throw new Exception("Unable to allocate LoadLibraryW parameter"); - this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter, out var err); + var err = this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter); this.extMemory.Read(this.loadLibraryRetPtr, out address); if (address == IntPtr.Zero) throw new Exception($"LoadLibraryW(\"{modulePath}\") failure: {new Win32Exception((int)err).Message} ({err})"); @@ -108,7 +108,7 @@ namespace Dalamud.Injector if (lpParameter == 0) throw new Exception("Unable to allocate GetProcAddress parameter ptr"); - this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter, out var err); + var err = this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter); this.extMemory.Read(this.getProcAddressRetPtr, out address); if (address == 0) throw new Exception($"GetProcAddress(0x{module:X}, \"{functionName}\") failure: {new Win32Exception((int)err).Message} ({err})"); @@ -119,27 +119,30 @@ namespace Dalamud.Injector ///

/// Method address. /// Parameter address. - /// Thread exit code. - public void CallRemoteFunction(nuint methodAddress, nuint parameterAddress, out uint exitCode) + /// Thread exit code. + public unsafe uint CallRemoteFunction(nuint methodAddress, nuint parameterAddress) { // Create and initialize a thread at our address and parameter address. - var threadHandle = CreateRemoteThread( - this.targetProcess.Handle, - IntPtr.Zero, + var threadHandle = Windows.Win32.PInvoke.CreateRemoteThread( + new HANDLE(this.targetProcess.Handle.ToPointer()), + null, UIntPtr.Zero, - methodAddress, - parameterAddress, - CreateThreadFlags.RunImmediately, - out _); + (delegate* unmanaged[Stdcall])methodAddress, + parameterAddress.ToPointer(), + 0, // Run immediately + null); if (threadHandle == IntPtr.Zero) throw new Exception($"CreateRemoteThread failure: {Marshal.GetLastWin32Error()}"); - _ = WaitForSingleObject(threadHandle, uint.MaxValue); + _ = Windows.Win32.PInvoke.WaitForSingleObject(threadHandle, uint.MaxValue); - GetExitCodeThread(threadHandle, out exitCode); + uint exitCode = 0; + if (!Windows.Win32.PInvoke.GetExitCodeThread(threadHandle, &exitCode)) + throw new Exception($"GetExitCodeThread failure: {Marshal.GetLastWin32Error()}"); - CloseHandle(threadHandle); + Windows.Win32.PInvoke.CloseHandle(threadHandle); + return exitCode; } private void SetupLoadLibrary(ProcessModule kernel32Module, ExportFunction[] kernel32Exports) diff --git a/Dalamud.Injector/NativeFunctions.cs b/Dalamud.Injector/NativeFunctions.cs deleted file mode 100644 index 06add3acc..000000000 --- a/Dalamud.Injector/NativeFunctions.cs +++ /dev/null @@ -1,956 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -namespace Dalamud.Injector -{ - /// - /// Native user32 functions. - /// - internal static partial class NativeFunctions - { - /// - /// MB_* from winuser. - /// - public enum MessageBoxType : uint - { - /// - /// The default value for any of the various subtypes. - /// - DefaultValue = 0x0, - - // To indicate the buttons displayed in the message box, specify one of the following values. - - /// - /// The message box contains three push buttons: Abort, Retry, and Ignore. - /// - AbortRetryIgnore = 0x2, - - /// - /// The message box contains three push buttons: Cancel, Try Again, Continue. Use this message box type instead - /// of MB_ABORTRETRYIGNORE. - /// - CancelTryContinue = 0x6, - - /// - /// Adds a Help button to the message box. When the user clicks the Help button or presses F1, the system sends - /// a WM_HELP message to the owner. - /// - Help = 0x4000, - - /// - /// The message box contains one push button: OK. This is the default. - /// - Ok = DefaultValue, - - /// - /// The message box contains two push buttons: OK and Cancel. - /// - OkCancel = 0x1, - - /// - /// The message box contains two push buttons: Retry and Cancel. - /// - RetryCancel = 0x5, - - /// - /// The message box contains two push buttons: Yes and No. - /// - YesNo = 0x4, - - /// - /// The message box contains three push buttons: Yes, No, and Cancel. - /// - YesNoCancel = 0x3, - - // To display an icon in the message box, specify one of the following values. - - /// - /// An exclamation-point icon appears in the message box. - /// - IconExclamation = 0x30, - - /// - /// An exclamation-point icon appears in the message box. - /// - IconWarning = IconExclamation, - - /// - /// An icon consisting of a lowercase letter i in a circle appears in the message box. - /// - IconInformation = 0x40, - - /// - /// An icon consisting of a lowercase letter i in a circle appears in the message box. - /// - IconAsterisk = IconInformation, - - /// - /// A question-mark icon appears in the message box. - /// The question-mark message icon is no longer recommended because it does not clearly represent a specific type - /// of message and because the phrasing of a message as a question could apply to any message type. In addition, - /// users can confuse the message symbol question mark with Help information. Therefore, do not use this question - /// mark message symbol in your message boxes. The system continues to support its inclusion only for backward - /// compatibility. - /// - IconQuestion = 0x20, - - /// - /// A stop-sign icon appears in the message box. - /// - IconStop = 0x10, - - /// - /// A stop-sign icon appears in the message box. - /// - IconError = IconStop, - - /// - /// A stop-sign icon appears in the message box. - /// - IconHand = IconStop, - - // To indicate the default button, specify one of the following values. - - /// - /// The first button is the default button. - /// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified. - /// - DefButton1 = DefaultValue, - - /// - /// The second button is the default button. - /// - DefButton2 = 0x100, - - /// - /// The third button is the default button. - /// - DefButton3 = 0x200, - - /// - /// The fourth button is the default button. - /// - DefButton4 = 0x300, - - // To indicate the modality of the dialog box, specify one of the following values. - - /// - /// The user must respond to the message box before continuing work in the window identified by the hWnd parameter. - /// However, the user can move to the windows of other threads and work in those windows. Depending on the hierarchy - /// of windows in the application, the user may be able to move to other windows within the thread. All child windows - /// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the - /// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified. - /// - ApplModal = DefaultValue, - - /// - /// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style. - /// Use system-modal message boxes to notify the user of serious, potentially damaging errors that require immediate - /// attention (for example, running out of memory). This flag has no effect on the user's ability to interact with - /// windows other than those associated with hWnd. - /// - SystemModal = 0x1000, - - /// - /// Same as MB_APPLMODAL except that all the top-level windows belonging to the current thread are disabled if the - /// hWnd parameter is NULL. Use this flag when the calling application or library does not have a window handle - /// available but still needs to prevent input to other windows in the calling thread without suspending other threads. - /// - TaskModal = 0x2000, - - // To specify other options, use one or more of the following values. - - /// - /// Same as desktop of the interactive window station. For more information, see Window Stations. If the current - /// input desktop is not the default desktop, MessageBox does not return until the user switches to the default - /// desktop. - /// - DefaultDesktopOnly = 0x20000, - - /// - /// The text is right-justified. - /// - Right = 0x80000, - - /// - /// Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems. - /// - RtlReading = 0x100000, - - /// - /// The message box becomes the foreground window. Internally, the system calls the SetForegroundWindow function - /// for the message box. - /// - SetForeground = 0x10000, - - /// - /// The message box is created with the WS_EX_TOPMOST window style. - /// - Topmost = 0x40000, - - /// - /// The caller is a service notifying the user of an event. The function displays a message box on the current active - /// desktop, even if there is no user logged on to the computer. - /// - ServiceNotification = 0x200000, - } - - /// - /// Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message, - /// such as status or error information. The message box returns an integer value that indicates which button the user - /// clicked. - /// - /// - /// A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no - /// owner window. - /// - /// - /// The message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage - /// return and/or linefeed character between each line. - /// - /// - /// The dialog box title. If this parameter is NULL, the default title is Error. - /// - /// The contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups - /// of flags. - /// - /// - /// If a message box has a Cancel button, the function returns the IDCANCEL value if either the ESC key is pressed or - /// the Cancel button is selected. If the message box has no Cancel button, pressing ESC will no effect - unless an - /// MB_OK button is present. If an MB_OK button is displayed and the user presses ESC, the return value will be IDOK. - /// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function - /// succeeds, the return value is one of the ID* enum values. - /// - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type); - } - - /// - /// Native kernel32 functions. - /// - internal static partial class NativeFunctions - { - /// - /// MEM_* from memoryapi. - /// - [Flags] - public enum AllocationType - { - /// - /// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce - /// placeholders, lpAddress and dwSize must exactly match those of the placeholder. - /// - CoalescePlaceholders = 0x1, - - /// - /// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using - /// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify - /// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER. - /// - PreservePlaceholder = 0x2, - - /// - /// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved - /// memory pages. The function also guarantees that when the caller later initially accesses the memory, the contents - /// will be zero. Actual physical pages are not allocated unless/until the virtual addresses are actually accessed. - /// To reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Attempting to commit - /// a specific address range by specifying MEM_COMMIT without MEM_RESERVE and a non-NULL lpAddress fails unless the - /// entire range has already been reserved. The resulting error code is ERROR_INVALID_ADDRESS. An attempt to commit - /// a page that is already committed does not cause the function to fail. This means that you can commit pages without - /// first determining the current commitment state of each page. If lpAddress specifies an address within an enclave, - /// flAllocationType must be MEM_COMMIT. - /// - Commit = 0x1000, - - /// - /// Reserves a range of the process's virtual address space without allocating any actual physical storage in memory - /// or in the paging file on disk. You commit reserved pages by calling VirtualAllocEx again with MEM_COMMIT. To - /// reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Other memory allocation - /// functions, such as malloc and LocalAlloc, cannot use reserved memory until it has been released. - /// - Reserve = 0x2000, - - /// - /// Decommits the specified region of committed pages. After the operation, the pages are in the reserved state. - /// The function does not fail if you attempt to decommit an uncommitted page. This means that you can decommit - /// a range of pages without first determining the current commitment state. The MEM_DECOMMIT value is not supported - /// when the lpAddress parameter provides the base address for an enclave. - /// - Decommit = 0x4000, - - /// - /// Releases the specified region of pages, or placeholder (for a placeholder, the address space is released and - /// available for other allocations). After this operation, the pages are in the free state. If you specify this - /// value, dwSize must be 0 (zero), and lpAddress must point to the base address returned by the VirtualAlloc function - /// when the region is reserved. The function fails if either of these conditions is not met. If any pages in the - /// region are committed currently, the function first decommits, and then releases them. The function does not - /// fail if you attempt to release pages that are in different states, some reserved and some committed. This means - /// that you can release a range of pages without first determining the current commitment state. - /// - Release = 0x8000, - - /// - /// Indicates that data in the memory range specified by lpAddress and dwSize is no longer of interest. The pages - /// should not be read from or written to the paging file. However, the memory block will be used again later, so - /// it should not be decommitted. This value cannot be used with any other value. Using this value does not guarantee - /// that the range operated on with MEM_RESET will contain zeros. If you want the range to contain zeros, decommit - /// the memory and then recommit it. When you use MEM_RESET, the VirtualAllocEx function ignores the value of fProtect. - /// However, you must still set fProtect to a valid protection value, such as PAGE_NOACCESS. VirtualAllocEx returns - /// an error if you use MEM_RESET and the range of memory is mapped to a file. A shared view is only acceptable - /// if it is mapped to a paging file. - /// - Reset = 0x80000, - - /// - /// MEM_RESET_UNDO should only be called on an address range to which MEM_RESET was successfully applied earlier. - /// It indicates that the data in the specified memory range specified by lpAddress and dwSize is of interest to - /// the caller and attempts to reverse the effects of MEM_RESET. If the function succeeds, that means all data in - /// the specified address range is intact. If the function fails, at least some of the data in the address range - /// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on - /// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the - /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid - /// protection value, such as PAGE_NOACCESS. - /// - ResetUndo = 0x1000000, - - /// - /// Reserves an address range that can be used to map Address Windowing Extensions (AWE) pages. This value must - /// be used with MEM_RESERVE and no other values. - /// - Physical = 0x400000, - - /// - /// Allocates memory at the highest possible address. This can be slower than regular allocations, especially when - /// there are many allocations. - /// - TopDown = 0x100000, - - /// - /// Causes the system to track pages that are written to in the allocated region. If you specify this value, you - /// must also specify MEM_RESERVE. To retrieve the addresses of the pages that have been written to since the region - /// was allocated or the write-tracking state was reset, call the GetWriteWatch function. To reset the write-tracking - /// state, call GetWriteWatch or ResetWriteWatch. The write-tracking feature remains enabled for the memory region - /// until the region is freed. - /// - WriteWatch = 0x200000, - - /// - /// Allocates memory using large page support. The size and alignment must be a multiple of the large-page minimum. - /// To obtain this value, use the GetLargePageMinimum function. If you specify this value, you must also specify - /// MEM_RESERVE and MEM_COMMIT. - /// - LargePages = 0x20000000, - } - - /// - /// Unprefixed flags from CreateRemoteThread. - /// - [Flags] - public enum CreateThreadFlags - { - /// - /// The thread runs immediately after creation. - /// - RunImmediately = 0x0, - - /// - /// The thread is created in a suspended state, and does not run until the ResumeThread function is called. - /// - CreateSuspended = 0x4, - - /// - /// The dwStackSize parameter specifies the initial reserve size of the stack. If this flag is not specified, dwStackSize specifies the commit size. - /// - StackSizeParamIsReservation = 0x10000, - } - - /// - /// DUPLICATE_* values for DuplicateHandle's dwDesiredAccess. - /// - [Flags] - public enum DuplicateOptions : uint - { - /// - /// Closes the source handle. This occurs regardless of any error status returned. - /// - CloseSource = 0x00000001, - - /// - /// Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle. - /// - SameAccess = 0x00000002, - } - - /// - /// PAGE_* from memoryapi. - /// - [Flags] - public enum MemoryProtection - { - /// - /// Enables execute access to the committed region of pages. An attempt to write to the committed region results - /// in an access violation. This flag is not supported by the CreateFileMapping function. - /// - Execute = 0x10, - - /// - /// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region - /// results in an access violation. - /// - ExecuteRead = 0x20, - - /// - /// Enables execute, read-only, or read/write access to the committed region of pages. - /// - ExecuteReadWrite = 0x40, - - /// - /// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to - /// write to a committed copy-on-write page results in a private copy of the page being made for the process. The - /// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not - /// supported by the VirtualAlloc or VirtualAllocEx functions. - /// - ExecuteWriteCopy = 0x80, - - /// - /// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed - /// region results in an access violation. This flag is not supported by the CreateFileMapping function. - /// - NoAccess = 0x01, - - /// - /// Enables read-only access to the committed region of pages. An attempt to write to the committed region results - /// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed - /// region results in an access violation. - /// - ReadOnly = 0x02, - - /// - /// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled, - /// attempting to execute code in the committed region results in an access violation. - /// - ReadWrite = 0x04, - - /// - /// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to - /// a committed copy-on-write page results in a private copy of the page being made for the process. The private - /// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is - /// enabled, attempting to execute code in the committed region results in an access violation. This flag is not - /// supported by the VirtualAlloc or VirtualAllocEx functions. - /// - WriteCopy = 0x08, - - /// - /// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like - /// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations - /// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable - /// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect - /// or CreateFileMapping functions. - /// - TargetsInvalid = 0x40000000, - - /// - /// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect. - /// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information - /// will be maintained while the page protection changes. This flag is only valid when the protection changes to - /// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. - /// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call - /// targets for CFG. - /// - TargetsNoUpdate = TargetsInvalid, - - /// - /// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a - /// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time - /// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn - /// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a - /// system service, the service typically returns a failure status indicator. This value cannot be used with - /// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function. - /// - Guard = 0x100, - - /// - /// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required - /// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an - /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS, - /// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the - /// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared - /// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function. - /// - NoCache = 0x200, - - /// - /// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required - /// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an - /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS, - /// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory - /// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access - /// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function. - /// - WriteCombine = 0x400, - } - - /// - /// PROCESS_* from processthreadsapi. - /// - [Flags] - public enum ProcessAccessFlags : uint - { - /// - /// All possible access rights for a process object. - /// - AllAccess = 0x001F0FFF, - - /// - /// Required to create a process. - /// - CreateProcess = 0x0080, - - /// - /// Required to create a thread. - /// - CreateThread = 0x0002, - - /// - /// Required to duplicate a handle using DuplicateHandle. - /// - DupHandle = 0x0040, - - /// - /// Required to retrieve certain information about a process, such as its token, exit code, - /// and priority class (see OpenProcessToken). - /// - QueryInformation = 0x0400, - - /// - /// Required to retrieve certain information about a process(see GetExitCodeProcess, GetPriorityClass, IsProcessInJob, - /// QueryFullProcessImageName). A handle that has the PROCESS_QUERY_INFORMATION access right is automatically granted - /// PROCESS_QUERY_LIMITED_INFORMATION. - /// - QueryLimitedInformation = 0x1000, - - /// - /// Required to set certain information about a process, such as its priority class (see SetPriorityClass). - /// - SetInformation = 0x0200, - - /// - /// Required to set memory limits using SetProcessWorkingSetSize. - /// - SetQuote = 0x0100, - - /// - /// Required to suspend or resume a process. - /// - SuspendResume = 0x0800, - - /// - /// Required to terminate a process using TerminateProcess. - /// - Terminate = 0x0001, - - /// - /// Required to perform an operation on the address space of a process(see VirtualProtectEx and WriteProcessMemory). - /// - VmOperation = 0x0008, - - /// - /// Required to read memory in a process using ReadProcessMemory. - /// - VmRead = 0x0010, - - /// - /// Required to write to memory in a process using WriteProcessMemory. - /// - VmWrite = 0x0020, - - /// - /// Required to wait for the process to terminate using the wait functions. - /// - Synchronize = 0x00100000, - } - - /// - /// WAIT_* from synchapi. - /// - public enum WaitResult - { - /// - /// The specified object is a mutex object that was not released by the thread that owned the mutex object - /// before the owning thread terminated.Ownership of the mutex object is granted to the calling thread and - /// the mutex state is set to nonsignaled. If the mutex was protecting persistent state information, you - /// should check it for consistency. - /// - Abandoned = 0x80, - - /// - /// The state of the specified object is signaled. - /// - Object0 = 0x0, - - /// - /// The time-out interval elapsed, and the object's state is nonsignaled. - /// - Timeout = 0x102, - - /// - /// The function has failed. To get extended error information, call GetLastError. - /// - WAIT_FAILED = 0xFFFFFFF, - } - - /// - /// Closes an open object handle. - /// - /// - /// A valid handle to an open object. - /// - /// - /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error - /// information, call GetLastError. If the application is running under a debugger, the function will throw an exception if it receives - /// either a handle value that is not valid or a pseudo-handle value. This can happen if you close a handle twice, or if you call - /// CloseHandle on a handle returned by the FindFirstFile function instead of calling the FindClose function. - /// - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CloseHandle(IntPtr hObject); - - /// - /// Creates a thread that runs in the virtual address space of another process. Use the CreateRemoteThreadEx function - /// to create a thread that runs in the virtual address space of another process and optionally specify extended attributes. - /// - /// - /// A handle to the process in which the thread is to be created. The handle must have the PROCESS_CREATE_THREAD, - /// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail without - /// these rights on certain platforms. For more information, see Process Security and Access Rights. - /// - /// - /// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines whether - /// child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default security descriptor - /// and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor for a thread come from - /// the primary token of the creator. - /// - /// - /// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is 0 (zero), the - /// new thread uses the default size for the executable. For more information, see Thread Stack Size. - /// - /// - /// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the - /// starting address of the thread in the remote process. The function must exist in the remote process. For more information, - /// see ThreadProc. - /// - /// - /// A pointer to a variable to be passed to the thread function. - /// - /// - /// The flags that control the creation of the thread. - /// - /// - /// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is not returned. - /// - /// - /// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value is - /// NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if lpStartAddress - /// points to data, code, or is not accessible. If the start address is invalid when the thread runs, an exception occurs, and - /// the thread terminates. Thread termination due to a invalid start address is handled as an error exit for the thread's process. - /// This behavior is similar to the asynchronous nature of CreateProcess, where the process is created even if it refers to - /// invalid or missing dynamic-link libraries (DLL). - /// - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr CreateRemoteThread( - IntPtr hProcess, - IntPtr lpThreadAttributes, - UIntPtr dwStackSize, - nuint lpStartAddress, - nuint lpParameter, - CreateThreadFlags dwCreationFlags, - out uint lpThreadId); - - /// - /// Retrieves the termination status of the specified thread. - /// - /// - /// A handle to the thread. The handle must have the THREAD_QUERY_INFORMATION or THREAD_QUERY_LIMITED_INFORMATION - /// access right.For more information, see Thread Security and Access Rights. - /// - /// - /// A pointer to a variable to receive the thread termination status. - /// - /// - /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get - /// extended error information, call GetLastError. - /// - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode); - - /// - /// Opens an existing local process object. - /// - /// - /// The access to the process object. This access right is checked against the security descriptor for the process. This parameter can be one or - /// more of the process access rights. If the caller has enabled the SeDebugPrivilege privilege, the requested access is granted regardless of the - /// contents of the security descriptor. - /// - /// - /// If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle. - /// - /// - /// The identifier of the local process to be opened. If the specified process is the System Idle Process(0x00000000), the function fails and the - /// last error code is ERROR_INVALID_PARAMETER.If the specified process is the System process or one of the Client Server Run-Time Subsystem(CSRSS) - /// processes, this function fails and the last error code is ERROR_ACCESS_DENIED because their access restrictions prevent user-level code from - /// opening them. If you are using GetCurrentProcessId as an argument to this function, consider using GetCurrentProcess instead of OpenProcess, for - /// improved performance. - /// - /// - /// If the function succeeds, the return value is an open handle to the specified process. - /// If the function fails, the return value is NULL.To get extended error information, call GetLastError. - /// - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr OpenProcess( - ProcessAccessFlags dwDesiredAccess, - bool bInheritHandle, - int dwProcessId); - - /// - /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex. - /// Reserves, commits, or changes the state of a region of memory within the virtual address space of a specified process. - /// The function initializes the memory it allocates to zero. To specify the NUMA node for the physical memory, see - /// VirtualAllocExNuma. - /// - /// - /// The handle to a process. The function allocates memory within the virtual address space of this process. The handle - /// must have the PROCESS_VM_OPERATION access right. For more information, see Process Security and Access Rights. - /// - /// - /// The pointer that specifies a desired starting address for the region of pages that you want to allocate. If you - /// are reserving memory, the function rounds this address down to the nearest multiple of the allocation granularity. - /// If you are committing memory that is already reserved, the function rounds this address down to the nearest page - /// boundary. To determine the size of a page and the allocation granularity on the host computer, use the GetSystemInfo - /// function. If lpAddress is NULL, the function determines where to allocate the region. If this address is within - /// an enclave that you have not initialized by calling InitializeEnclave, VirtualAllocEx allocates a page of zeros - /// for the enclave at that address. The page must be previously uncommitted, and will not be measured with the EEXTEND - /// instruction of the Intel Software Guard Extensions programming model. If the address in within an enclave that you - /// initialized, then the allocation operation fails with the ERROR_INVALID_ADDRESS error. - /// - /// - /// The size of the region of memory to allocate, in bytes. If lpAddress is NULL, the function rounds dwSize up to the - /// next page boundary. If lpAddress is not NULL, the function allocates all pages that contain one or more bytes in - /// the range from lpAddress to lpAddress+dwSize. This means, for example, that a 2-byte range that straddles a page - /// boundary causes the function to allocate both pages. - /// - /// - /// The type of memory allocation. This parameter must contain one of the MEM_* enum values. - /// - /// - /// The memory protection for the region of pages to be allocated. If the pages are being committed, you can specify - /// any one of the memory protection constants. - /// - /// - /// If the function succeeds, the return value is the base address of the allocated region of pages. If the function - /// fails, the return value is NULL.To get extended error information, call GetLastError. - /// - [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] - public static extern IntPtr VirtualAllocEx( - IntPtr hProcess, - IntPtr lpAddress, - int dwSize, - AllocationType flAllocationType, - MemoryProtection flProtect); - - /// - /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualfreeex. - /// Releases, decommits, or releases and decommits a region of memory within the virtual address space of a specified - /// process. - /// - /// - /// A handle to a process. The function frees memory within the virtual address space of the process. The handle must - /// have the PROCESS_VM_OPERATION access right.For more information, see Process Security and Access Rights. - /// - /// - /// A pointer to the starting address of the region of memory to be freed. If the dwFreeType parameter is MEM_RELEASE, - /// lpAddress must be the base address returned by the VirtualAllocEx function when the region is reserved. - /// - /// - /// The size of the region of memory to free, in bytes. If the dwFreeType parameter is MEM_RELEASE, dwSize must be 0 - /// (zero). The function frees the entire region that is reserved in the initial allocation call to VirtualAllocEx. - /// If dwFreeType is MEM_DECOMMIT, the function decommits all memory pages that contain one or more bytes in the range - /// from the lpAddress parameter to (lpAddress+dwSize). This means, for example, that a 2-byte region of memory that - /// straddles a page boundary causes both pages to be decommitted. If lpAddress is the base address returned by - /// VirtualAllocEx and dwSize is 0 (zero), the function decommits the entire region that is allocated by VirtualAllocEx. - /// After that, the entire region is in the reserved state. - /// - /// - /// The type of free operation. This parameter must be one of the MEM_* enum values. - /// - /// - /// If the function succeeds, the return value is a nonzero value. If the function fails, the return value is 0 (zero). - /// To get extended error information, call GetLastError. - /// - [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] - public static extern bool VirtualFreeEx( - IntPtr hProcess, - IntPtr lpAddress, - int dwSize, - AllocationType dwFreeType); - - /// - /// Waits until the specified object is in the signaled state or the time-out interval elapses. To enter an alertable wait - /// state, use the WaitForSingleObjectEx function.To wait for multiple objects, use WaitForMultipleObjects. - /// - /// - /// A handle to the object. For a list of the object types whose handles can be specified, see the following Remarks section. - /// If this handle is closed while the wait is still pending, the function's behavior is undefined. The handle must have the - /// SYNCHRONIZE access right. For more information, see Standard Access Rights. - /// - /// - /// The time-out interval, in milliseconds. If a nonzero value is specified, the function waits until the object is signaled - /// or the interval elapses. If dwMilliseconds is zero, the function does not enter a wait state if the object is not signaled; - /// it always returns immediately. If dwMilliseconds is INFINITE, the function will return only when the object is signaled. - /// - /// - /// If the function succeeds, the return value indicates the event that caused the function to return. - /// It can be one of the WaitResult values. - /// - [DllImport("kernel32.dll", SetLastError = true)] - public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); - - /// - /// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or - /// the operation fails. - /// - /// - /// A handle to the process memory to be modified. The handle must have PROCESS_VM_WRITE and PROCESS_VM_OPERATION access - /// to the process. - /// - /// - /// A pointer to the base address in the specified process to which data is written. Before data transfer occurs, the - /// system verifies that all data in the base address and memory of the specified size is accessible for write access, - /// and if it is not accessible, the function fails. - /// - /// - /// A pointer to the buffer that contains data to be written in the address space of the specified process. - /// - /// - /// The number of bytes to be written to the specified process. - /// - /// - /// A pointer to a variable that receives the number of bytes transferred into the specified process. This parameter - /// is optional. If lpNumberOfBytesWritten is NULL, the parameter is ignored. - /// - /// - /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get - /// extended error information, call GetLastError.The function fails if the requested write operation crosses into an - /// area of the process that is inaccessible. - /// - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool WriteProcessMemory( - IntPtr hProcess, - IntPtr lpBaseAddress, - byte[] lpBuffer, - int dwSize, - out IntPtr lpNumberOfBytesWritten); - - /// - /// Duplicates an object handle. - /// - /// - /// A handle to the process with the handle to be duplicated. - /// - /// The handle must have the PROCESS_DUP_HANDLE access right. - /// - /// - /// The handle to be duplicated. This is an open object handle that is valid in the context of the source process. - /// For a list of objects whose handles can be duplicated, see the following Remarks section. - /// - /// - /// A handle to the process that is to receive the duplicated handle. - /// - /// The handle must have the PROCESS_DUP_HANDLE access right. - /// - /// - /// A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process. - /// - /// If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively. - /// - /// If lpTargetHandle is NULL, the function duplicates the handle, but does not return the duplicate handle value to the caller. This behavior exists only for backward compatibility with previous versions of this function. You should not use this feature, as you will lose system resources until the target process terminates. - /// - /// This parameter is ignored if hTargetProcessHandle is NULL. - /// - /// - /// The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section. - /// - /// This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag. Otherwise, the flags that can be specified depend on the type of object whose handle is to be duplicated. - /// - /// This parameter is ignored if hTargetProcessHandle is NULL. - /// - /// - /// A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited. - /// - /// This parameter is ignored if hTargetProcessHandle is NULL. - /// - /// - /// Optional actions. - /// - /// - /// If the function succeeds, the return value is nonzero. - /// - /// If the function fails, the return value is zero. To get extended error information, call GetLastError. - /// - /// - /// See https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle. - /// - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool DuplicateHandle( - IntPtr hSourceProcessHandle, - IntPtr hSourceHandle, - IntPtr hTargetProcessHandle, - out IntPtr lpTargetHandle, - uint dwDesiredAccess, - [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, - DuplicateOptions dwOptions); - - /// - /// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew. - /// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To - /// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function. - /// - /// - /// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default - /// library extension .dll is appended. The file name string can include a trailing point character (.) to indicate - /// that the module name has no extension. The string does not have to specify a path. When specifying a path, be sure - /// to use backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules - /// currently mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns - /// a handle to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve - /// handles for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx. - /// - /// - /// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return - /// value is NULL.To get extended error information, call GetLastError. - /// - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] - public static extern IntPtr GetModuleHandleW(string lpModuleName); - - /// - /// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL). - /// - /// - /// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary, - /// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules - /// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx. - /// - /// - /// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be - /// in the low-order word; the high-order word must be zero. - /// - /// - /// If the function succeeds, the return value is the address of the exported function or variable. If the function - /// fails, the return value is NULL.To get extended error information, call GetLastError. - /// - [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] - [SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")] - public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); - } -} diff --git a/Dalamud.Injector/NativeMethods.json b/Dalamud.Injector/NativeMethods.json new file mode 100644 index 000000000..ffb313dfc --- /dev/null +++ b/Dalamud.Injector/NativeMethods.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "allowMarshaling": false +} diff --git a/Dalamud.Injector/NativeMethods.txt b/Dalamud.Injector/NativeMethods.txt new file mode 100644 index 000000000..ffcf192a2 --- /dev/null +++ b/Dalamud.Injector/NativeMethods.txt @@ -0,0 +1,8 @@ +CreateRemoteThread +WaitForSingleObject +GetExitCodeThread +DuplicateHandle + +MessageBox +GetModuleHandle +GetProcAddress From 7b723687a447bb0f90da5d5c025a073b9120a063 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 15 Oct 2025 22:57:43 +0200 Subject: [PATCH 095/477] Use NuGet CPM to ensure consistent package versions Fixes various issues with projects referencing different versions of libraries, causing deployment issues if build order differs --- Dalamud.Common/Dalamud.Common.csproj | 2 +- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 6 +- Dalamud.Injector/Dalamud.Injector.csproj | 24 +++---- Dalamud.Test/Dalamud.Test.csproj | 20 +++--- Dalamud.sln | 1 + Dalamud/Dalamud.csproj | 52 ++++++++------- Directory.Build.props | 8 +-- Directory.Packages.props | 65 +++++++++++++++++++ build/build.csproj | 1 + .../Dalamud.Bindings.ImGui.csproj | 2 +- .../Dalamud.Bindings.ImGuizmo.csproj | 2 +- .../Dalamud.Bindings.ImPlot.csproj | 2 +- .../StandaloneImGuiTestbed.csproj | 3 +- .../Dalamud.LocExporter.csproj | 4 +- 14 files changed, 126 insertions(+), 66 deletions(-) create mode 100644 Directory.Packages.props diff --git a/Dalamud.Common/Dalamud.Common.csproj b/Dalamud.Common/Dalamud.Common.csproj index 54a182210..774b551ad 100644 --- a/Dalamud.Common/Dalamud.Common.csproj +++ b/Dalamud.Common/Dalamud.Common.csproj @@ -6,7 +6,7 @@ - + diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 0c6a86e8d..9b355a40b 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -25,9 +25,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index e4b46ccab..4a55174a1 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -52,18 +52,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud.Test/Dalamud.Test.csproj b/Dalamud.Test/Dalamud.Test.csproj index c6c8f8e8a..488534e5e 100644 --- a/Dalamud.Test/Dalamud.Test.csproj +++ b/Dalamud.Test/Dalamud.Test.csproj @@ -42,19 +42,19 @@ - - - - - - - - - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud.sln b/Dalamud.sln index fe1c9b19d..c3af00f44 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets tools\dalamud.ruleset = tools\dalamud.ruleset Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}" diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index b4cfd0a35..75ec61ec3 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -61,37 +61,38 @@ - - - - - - - - - + + + + + + + + + all - - - - - - - - - - + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + + + imgui-frag.hlsl.bytes @@ -100,6 +101,7 @@ imgui-vertex.hlsl.bytes + diff --git a/Directory.Build.props b/Directory.Build.props index b5330709f..4ed87c809 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,15 +8,9 @@ 13.0 - - - 6.5.1 - 13.0.3 - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build.csproj b/build/build.csproj index 37a4d3252..b4aaa959d 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -8,6 +8,7 @@ .. 1 true + false diff --git a/imgui/Dalamud.Bindings.ImGui/Dalamud.Bindings.ImGui.csproj b/imgui/Dalamud.Bindings.ImGui/Dalamud.Bindings.ImGui.csproj index fc5894857..8668e9b8f 100644 --- a/imgui/Dalamud.Bindings.ImGui/Dalamud.Bindings.ImGui.csproj +++ b/imgui/Dalamud.Bindings.ImGui/Dalamud.Bindings.ImGui.csproj @@ -9,7 +9,7 @@ - + diff --git a/imgui/Dalamud.Bindings.ImGuizmo/Dalamud.Bindings.ImGuizmo.csproj b/imgui/Dalamud.Bindings.ImGuizmo/Dalamud.Bindings.ImGuizmo.csproj index 650381100..6cdd80470 100644 --- a/imgui/Dalamud.Bindings.ImGuizmo/Dalamud.Bindings.ImGuizmo.csproj +++ b/imgui/Dalamud.Bindings.ImGuizmo/Dalamud.Bindings.ImGuizmo.csproj @@ -9,7 +9,7 @@ - + diff --git a/imgui/Dalamud.Bindings.ImPlot/Dalamud.Bindings.ImPlot.csproj b/imgui/Dalamud.Bindings.ImPlot/Dalamud.Bindings.ImPlot.csproj index 650381100..6cdd80470 100644 --- a/imgui/Dalamud.Bindings.ImPlot/Dalamud.Bindings.ImPlot.csproj +++ b/imgui/Dalamud.Bindings.ImPlot/Dalamud.Bindings.ImPlot.csproj @@ -9,7 +9,7 @@ - + diff --git a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj index 21a6a4c66..d56faa31e 100644 --- a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj +++ b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj @@ -1,10 +1,10 @@  - Exe enable enable true + false @@ -36,5 +36,4 @@ - diff --git a/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj b/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj index aa6cabedc..c0d31c3d6 100644 --- a/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj +++ b/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj @@ -1,5 +1,4 @@  - Exe enable @@ -11,7 +10,6 @@ - + - From 76afcaf4261ffa078327d0b79f5fb57012ae0725 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 15 Oct 2025 23:24:21 +0200 Subject: [PATCH 096/477] Disable CPM for libs --- lib/Directory.Packages.props | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 lib/Directory.Packages.props diff --git a/lib/Directory.Packages.props b/lib/Directory.Packages.props new file mode 100644 index 000000000..bafcac533 --- /dev/null +++ b/lib/Directory.Packages.props @@ -0,0 +1,5 @@ + + + false + + From 2be6566e81c30e55eb39246536826b5c4b5d9234 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 16 Oct 2025 20:34:20 +0200 Subject: [PATCH 097/477] Fix broken spacing in update chat message --- Dalamud/Game/ChatHandlers.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index c57dd70b8..b5639c4d3 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -124,11 +124,11 @@ internal partial class ChatHandlers : IServiceType var updateMessage = new SeStringBuilder() .AddText(Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully!")) .AddUiForeground(500) - .AddText(" [") + .AddText(" [ ") .Add(linkPayload) - .AddText(Loc.Localize("DalamudClickToViewChangelogs", " Click here to view the changelog.")) + .AddText(Loc.Localize("DalamudClickToViewChangelogs", "Click here to view the changelog.")) .Add(RawPayload.LinkTerminator) - .AddText("]") + .AddText(" ]") .AddUiForegroundOff(); chatGui.Print(new XivChatEntry From 116e8aadbc641472c69ab6ae6ea87d4640b5cc59 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 16 Oct 2025 21:10:27 +0200 Subject: [PATCH 098/477] build: 13.0.0.6 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 75ec61ec3..371235c89 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.5 + 13.0.0.6 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 0b6f3b8bcfaf9f4ab8fb1713097b9930319bd27f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 1 Oct 2025 15:30:29 +0200 Subject: [PATCH 099/477] Add events based on AgentUpdateFlag --- Dalamud/Game/Gui/GameGui.cs | 75 +++++++++++++++++++++++++ Dalamud/Game/Inventory/GameInventory.cs | 25 +++------ Dalamud/Plugin/Services/IGameGui.cs | 21 +++++++ 3 files changed, 103 insertions(+), 18 deletions(-) diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 4da7b6857..51ab6b1d2 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -43,6 +43,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui private readonly Hook handleImmHook; private readonly Hook setUiVisibilityHook; private readonly Hook utf8StringFromSequenceHook; + private readonly Hook raptureAtkModuleUpdateHook; [ServiceManager.ServiceConstructor] private GameGui(TargetSigScanner sigScanner) @@ -65,6 +66,10 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui this.utf8StringFromSequenceHook = Hook.FromAddress(Utf8String.Addresses.Ctor_FromSequence.Value, this.Utf8StringFromSequenceDetour); + this.raptureAtkModuleUpdateHook = Hook.FromFunctionPointerVariable( + new(&RaptureAtkModule.StaticVirtualTablePointer->Update), + this.RaptureAtkModuleUpdateDetour); + this.handleItemHoverHook.Enable(); this.handleItemOutHook.Enable(); this.handleImmHook.Enable(); @@ -72,6 +77,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui this.handleActionHoverHook.Enable(); this.handleActionOutHook.Enable(); this.utf8StringFromSequenceHook.Enable(); + this.raptureAtkModuleUpdateHook.Enable(); } // Hooked delegates @@ -88,6 +94,18 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui /// public event EventHandler? HoveredActionChanged; + /// + public event Action InventoryUpdate; + + /// + public event Action ActionBarUpdate; + + /// + public event Action UnlocksUpdate; + + /// + public event Action MainCommandEnabledStateUpdate; + /// public bool GameUiHidden { get; private set; } @@ -238,6 +256,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui this.handleActionHoverHook.Dispose(); this.handleActionOutHook.Dispose(); this.utf8StringFromSequenceHook.Dispose(); + this.raptureAtkModuleUpdateHook.Dispose(); } /// @@ -362,6 +381,34 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui return thisPtr; // this function shouldn't need to return but the original asm moves this into rax before returning so be safe? } + + private void RaptureAtkModuleUpdateDetour(RaptureAtkModule* thisPtr, float delta) + { + // The game clears the AgentUpdateFlag in the original function, but it also updates agents in it too. + // We'll make a copy of the flags, so that we can fire events after the agents have been updated. + + var agentUpdateFlag = thisPtr->AgentUpdateFlag; + + this.raptureAtkModuleUpdateHook.Original(thisPtr, delta); + + if (agentUpdateFlag != RaptureAtkModule.AgentUpdateFlags.None) + { + if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.InventoryUpdate) || + agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.RetainerMarketInventoryUpdate) || + agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.HousingInventoryUpdate) || + agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.ContentInventoryUpdate)) + this.InventoryUpdate.InvokeSafely(); + + if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.ActionBarUpdate)) + this.ActionBarUpdate.InvokeSafely(); + + if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.UnlocksUpdate)) + this.UnlocksUpdate.InvokeSafely(); + + if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.MainCommandEnabledStateUpdate)) + this.MainCommandEnabledStateUpdate.InvokeSafely(); + } + } } /// @@ -385,6 +432,10 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui this.gameGuiService.UiHideToggled += this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged += this.HoveredItemForward; this.gameGuiService.HoveredActionChanged += this.HoveredActionForward; + this.gameGuiService.InventoryUpdate += this.InventoryUpdateForward; + this.gameGuiService.ActionBarUpdate += this.ActionBarUpdateForward; + this.gameGuiService.UnlocksUpdate += this.UnlocksUpdateForward; + this.gameGuiService.MainCommandEnabledStateUpdate += this.MainCommandEnabledStateUpdateForward; } /// @@ -396,6 +447,18 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui /// public event EventHandler? HoveredActionChanged; + /// + public event Action InventoryUpdate; + + /// + public event Action ActionBarUpdate; + + /// + public event Action UnlocksUpdate; + + /// + public event Action MainCommandEnabledStateUpdate; + /// public bool GameUiHidden => this.gameGuiService.GameUiHidden; @@ -415,6 +478,10 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; this.gameGuiService.HoveredActionChanged -= this.HoveredActionForward; + this.gameGuiService.InventoryUpdate -= this.InventoryUpdateForward; + this.gameGuiService.ActionBarUpdate -= this.ActionBarUpdateForward; + this.gameGuiService.UnlocksUpdate -= this.UnlocksUpdateForward; + this.gameGuiService.MainCommandEnabledStateUpdate -= this.MainCommandEnabledStateUpdateForward; this.UiHideToggled = null; this.HoveredItemChanged = null; @@ -466,4 +533,12 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui private void HoveredItemForward(object sender, ulong itemId) => this.HoveredItemChanged?.Invoke(sender, itemId); private void HoveredActionForward(object sender, HoveredAction hoverAction) => this.HoveredActionChanged?.Invoke(sender, hoverAction); + + private void InventoryUpdateForward() => this.InventoryUpdate.InvokeSafely(); + + private void ActionBarUpdateForward() => this.ActionBarUpdate.InvokeSafely(); + + private void UnlocksUpdateForward() => this.UnlocksUpdate.InvokeSafely(); + + private void MainCommandEnabledStateUpdateForward() => this.MainCommandEnabledStateUpdate.InvokeSafely(); } diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 5c8ed27b0..e2e3c1ec2 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -2,16 +2,14 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Dalamud.Game.Gui; using Dalamud.Game.Inventory.InventoryEventArgTypes; -using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.UI; - namespace Dalamud.Game.Inventory; /// @@ -33,7 +31,8 @@ internal class GameInventory : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - private readonly Hook raptureAtkModuleUpdateHook; + [ServiceManager.ServiceDependency] + private readonly GameGui gameGui = Service.Get(); private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; @@ -47,18 +46,9 @@ internal class GameInventory : IInternalDisposableService this.inventoryTypes = Enum.GetValues(); this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; - unsafe - { - this.raptureAtkModuleUpdateHook = Hook.FromFunctionPointerVariable( - new(&RaptureAtkModule.StaticVirtualTablePointer->Update), - this.RaptureAtkModuleUpdateDetour); - } - - this.raptureAtkModuleUpdateHook.Enable(); + this.gameGui.InventoryUpdate += this.OnInventoryUpdate; } - private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1); - /// void IInternalDisposableService.DisposeService() { @@ -68,7 +58,7 @@ internal class GameInventory : IInternalDisposableService this.subscribersPendingChange.Clear(); this.subscribersChanged = false; this.framework.Update -= this.OnFrameworkUpdate; - this.raptureAtkModuleUpdateHook.Dispose(); + this.gameGui.InventoryUpdate -= this.OnInventoryUpdate; } } @@ -319,10 +309,9 @@ internal class GameInventory : IInternalDisposableService return items; } - private unsafe void RaptureAtkModuleUpdateDetour(RaptureAtkModule* ram, float f1) + private void OnInventoryUpdate() { - this.inventoriesMightBeChanged |= ram->AgentUpdateFlag != 0; - this.raptureAtkModuleUpdateHook.Original(ram, f1); + this.inventoriesMightBeChanged |= true; } /// diff --git a/Dalamud/Plugin/Services/IGameGui.cs b/Dalamud/Plugin/Services/IGameGui.cs index e2326328a..fc37c14b7 100644 --- a/Dalamud/Plugin/Services/IGameGui.cs +++ b/Dalamud/Plugin/Services/IGameGui.cs @@ -26,6 +26,27 @@ public unsafe interface IGameGui /// public event EventHandler HoveredActionChanged; + /// + /// Event that is fired when the inventory has been updated. + /// + event Action InventoryUpdate; + + /// + /// Fired when the action bar needs to be updated, e.g. after changing Class/Job, + /// updating Gear Sets, modifying Macros, or executing a hotbar slot. + /// + event Action ActionBarUpdate; + + /// + /// Event that is fired when collectibles, content or systems were unlocked. + /// + event Action UnlocksUpdate; + + /// + /// Event that is fired when the enable state of MainCommands has been updated. + /// + event Action MainCommandEnabledStateUpdate; + /// /// Gets a value indicating whether the game UI is hidden. /// From 8ed21b46457b93930fa90e173ed97818b5b8c65d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 22 Oct 2025 07:16:13 +0200 Subject: [PATCH 100/477] Public ImRaii end objects --- Dalamud/Interface/Utility/Raii/EndObjects.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Utility/Raii/EndObjects.cs b/Dalamud/Interface/Utility/Raii/EndObjects.cs index f84844dda..8243c7dfb 100644 --- a/Dalamud/Interface/Utility/Raii/EndObjects.cs +++ b/Dalamud/Interface/Utility/Raii/EndObjects.cs @@ -242,7 +242,7 @@ public static partial class ImRaii // Use end-function regardless of success. // Used by Child, Group and Tooltip. - private struct EndUnconditionally : IEndObject + public struct EndUnconditionally : IEndObject { private Action EndAction { get; } @@ -268,7 +268,7 @@ public static partial class ImRaii } // Use end-function only on success. - private struct EndConditionally : IEndObject + public struct EndConditionally : IEndObject { public EndConditionally(Action endAction, bool success) { From f0568216cbc802d594d4f9eb9d79b26ef358b679 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 22 Oct 2025 07:18:43 +0200 Subject: [PATCH 101/477] Provide PopupModal with flags --- Dalamud/Interface/Utility/Raii/EndObjects.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dalamud/Interface/Utility/Raii/EndObjects.cs b/Dalamud/Interface/Utility/Raii/EndObjects.cs index 8243c7dfb..80122360c 100644 --- a/Dalamud/Interface/Utility/Raii/EndObjects.cs +++ b/Dalamud/Interface/Utility/Raii/EndObjects.cs @@ -40,6 +40,9 @@ public static partial class ImRaii public static IEndObject PopupModal(ImU8String id) => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupModal(id)); + public static IEndObject PopupModal(ImU8String id, ImGuiWindowFlags flags) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupModal(id, flags)); + public static IEndObject PopupModal(ImU8String id, ref bool open) => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupModal(id, ref open)); From 87e391958effe453dc3df25587f7d44eb536f822 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 22 Oct 2025 19:14:29 +0200 Subject: [PATCH 102/477] Lock plugin list in NotifyPluginsForStateChange --- Dalamud/Plugin/Internal/PluginManager.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index db803caa8..4dee52b7e 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1299,13 +1299,16 @@ internal class PluginManager : IInternalDisposableService /// The affected plugins. public void NotifyPluginsForStateChange(PluginListInvalidationKind kind, IEnumerable affectedInternalNames) { - foreach (var installedPlugin in this.installedPluginsList) + lock (this.pluginListLock) { - if (!installedPlugin.IsLoaded || installedPlugin.DalamudInterface == null) - continue; + foreach (var installedPlugin in this.installedPluginsList) + { + if (!installedPlugin.IsLoaded || installedPlugin.DalamudInterface == null) + continue; - installedPlugin.DalamudInterface.NotifyActivePluginsChanged( - new ActivePluginsChangedEventArgs(kind, affectedInternalNames)); + installedPlugin.DalamudInterface.NotifyActivePluginsChanged( + new ActivePluginsChangedEventArgs(kind, affectedInternalNames)); + } } } From 39e6186ba37aa2bcc79b86ab6480b59c6fb8bcf7 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 24 Oct 2025 03:07:36 +0200 Subject: [PATCH 103/477] Fix AddonEventType.Resize --- Dalamud/Game/Addon/Events/AddonEventType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventType.cs b/Dalamud/Game/Addon/Events/AddonEventType.cs index 2c88c797b..25beb13fc 100644 --- a/Dalamud/Game/Addon/Events/AddonEventType.cs +++ b/Dalamud/Game/Addon/Events/AddonEventType.cs @@ -74,7 +74,7 @@ public enum AddonEventType : byte /// /// Resize (ChatLogPanel). /// - Resize = 19, + Resize = 21, /// /// AtkComponentButton Press, sent on MouseDown on Button. From 4fff7dee5a89b30d38cac88ef250b8f9ff54250d Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:20:31 +0900 Subject: [PATCH 104/477] Add more null checks to NamePlateUpdateHander.GameObject --- Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs index 185be9d24..e2b2e4191 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs @@ -340,9 +340,13 @@ internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler return null; } - return this.gameObject ??= this.context.ObjectTable[ - this.context.Ui3DModule->NamePlateObjectInfoPointers[this.ArrayIndex] - .Value->GameObject->ObjectIndex]; + var objectInfoPtr = this.context.Ui3DModule->NamePlateObjectInfoPointers[this.ArrayIndex]; + if (objectInfoPtr.Value == null) return null; + + var gameObjectPtr = objectInfoPtr.Value->GameObject; + if (gameObjectPtr == null) return null; + + return this.gameObject ??= this.context.ObjectTable[gameObjectPtr->ObjectIndex]; } } From de07a5f1b7cc7c2f23a04a0c7caa0ec9dcc12947 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 28 Oct 2025 16:15:56 -0700 Subject: [PATCH 105/477] fix: missing args in HandleActionHoverHook (#2440) - remove verbose logs --- Dalamud/Game/Gui/GameGui.cs | 12 ++---------- lib/FFXIVClientStructs | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 4da7b6857..df01dd95d 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -280,8 +280,6 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui this.HoveredItem = itemId; this.HoveredItemChanged?.InvokeSafely(this, itemId); - - Log.Verbose($"HoveredItem changed: {itemId}"); } private AtkValue* HandleItemOutDetour(AgentItemDetail* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) @@ -292,22 +290,18 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui { this.HoveredItem = 0; this.HoveredItemChanged?.InvokeSafely(this, 0ul); - - Log.Verbose("HoveredItem changed: 0"); } return ret; } - private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, bool a5) + private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, bool a5, int a6, int a7) { - this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5); + this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5, a6, a7); this.HoveredAction.ActionKind = (HoverActionKind)actionKind; this.HoveredAction.BaseActionID = actionId; this.HoveredAction.ActionID = hoverState->ActionId; this.HoveredActionChanged?.InvokeSafely(this, this.HoveredAction); - - Log.Verbose($"HoverActionId: {actionKind}/{actionId} this:{(nint)hoverState:X}"); } private AtkValue* HandleActionOutDetour(AgentActionDetail* agentActionDetail, AtkValue* a2, AtkValue* a3, uint a4, ulong a5) @@ -324,8 +318,6 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui this.HoveredAction.BaseActionID = 0; this.HoveredAction.ActionID = 0; this.HoveredActionChanged?.InvokeSafely(this, this.HoveredAction); - - Log.Verbose("HoverActionId: 0"); } } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index bf9759183..c1a76eb2a 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit bf9759183bc9ef50e5d1ccfafcefdde8585612c7 +Subproject commit c1a76eb2afab08457f77dca91f4cef2b050dc949 From 11e85c66194e64fad74f99d810c1795b2d99d23b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Oct 2025 23:17:54 +0000 Subject: [PATCH 106/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index c1a76eb2a..2dfa06800 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit c1a76eb2afab08457f77dca91f4cef2b050dc949 +Subproject commit 2dfa068007695c0177dbb362034c6a26e036069f From 76dab05cbd58b8fc7183b0d8aaf64cbab057f191 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 29 Oct 2025 02:11:46 +0100 Subject: [PATCH 107/477] Allow auto updates when on Free Trial --- Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 169864bdf..23ddbd993 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -497,7 +497,8 @@ internal class AutoUpdateManager : IServiceType condition.OnlyAny(ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, - ConditionFlag.UsingFashionAccessory); + ConditionFlag.UsingFashionAccessory, + ConditionFlag.OnFreeTrial); } private bool IsPluginManagerReady() From e6df536ceb0af1d1fd3cc89ff1188a092692a957 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 29 Oct 2025 02:13:58 +0100 Subject: [PATCH 108/477] Don't always flag as idle when on Free Trial --- Dalamud/Game/ClientState/ClientState.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index e92af21c3..d90308e55 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -249,7 +249,8 @@ internal sealed class ClientState : IInternalDisposableService, IClientState ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, - ConditionFlag.UsingFashionAccessory]); + ConditionFlag.UsingFashionAccessory, + ConditionFlag.OnFreeTrial]); blockingFlag = blockingConditions.FirstOrDefault(); return blockingFlag == 0; From 08a39988547f41574616c98cc040480fdce87f18 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 29 Oct 2025 02:31:42 +0100 Subject: [PATCH 109/477] Use IsClientIdle in CanUpdateOrNag --- Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 23ddbd993..3fc011a68 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -491,14 +491,10 @@ internal class AutoUpdateManager : IServiceType private bool CanUpdateOrNag() { - var condition = Service.Get(); + var clientState = Service.Get(); return this.IsPluginManagerReady() && !this.dalamudInterface.IsPluginInstallerOpen && - condition.OnlyAny(ConditionFlag.NormalConditions, - ConditionFlag.Jumping, - ConditionFlag.Mounted, - ConditionFlag.UsingFashionAccessory, - ConditionFlag.OnFreeTrial); + clientState.IsClientIdle(); } private bool IsPluginManagerReady() From 55246ab1ecc80ec6f5450337cab87bcf445c6a00 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 19 Oct 2025 18:42:24 +0200 Subject: [PATCH 110/477] Enable sound effects by default for new users --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 08bbeb938..241a08d90 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -250,7 +250,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// This setting is effected by the in-game "System Sounds" option and volume. /// [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")] - public bool EnablePluginUISoundEffects { get; set; } + public bool EnablePluginUISoundEffects { get; set; } = true; /// /// Gets or sets a value indicating whether an additional button allowing pinning and clickthrough options should be shown From 606d58c77e616f3814f294e6a6195079e3e0cac2 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 29 Oct 2025 20:57:06 +0100 Subject: [PATCH 111/477] build: 13.0.0.7 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 371235c89..890202967 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.6 + 13.0.0.7 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From ce16b59f5b09b805c00b2826397153d77e13f731 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 2 Nov 2025 15:40:22 +0100 Subject: [PATCH 112/477] Fix ImDrawCallback call --- Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs index 3f6fc46de..2eb4e4970 100644 --- a/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs +++ b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs @@ -380,8 +380,8 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer default: { // User callback, registered via ImDrawList::AddCallback() - var cb = (delegate*)cmd.UserCallback; - cb(cmdList, ref cmd); + var cb = (delegate* unmanaged)cmd.UserCallback; + cb(cmdList, (ImDrawCmdPtr)Unsafe.AsPointer(ref cmd)); break; } } From 417fe39cd9e8355ac60731211634d14eb88b875e Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:28:34 +0900 Subject: [PATCH 113/477] Add yet another check to NamePlateUpdateHander.GameObject --- Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs index e2b2e4191..a8c6ff3c1 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs @@ -340,6 +340,9 @@ internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler return null; } + if (this.ArrayIndex >= this.context.Ui3DModule->NamePlateObjectInfoCount) + return null; + var objectInfoPtr = this.context.Ui3DModule->NamePlateObjectInfoPointers[this.ArrayIndex]; if (objectInfoPtr.Value == null) return null; From 7751ea71850e18b61ee5c3ad3ad9cccc507225ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Nov 2025 18:35:02 +0000 Subject: [PATCH 114/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 2dfa06800..2b2d67e37 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 2dfa068007695c0177dbb362034c6a26e036069f +Subproject commit 2b2d67e374b9f117d3d8038070bd80909de3941d From 8a49a11dc0c8b6de8ec53e76c822bcefca8a193a Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 4 Nov 2025 12:18:38 -0800 Subject: [PATCH 115/477] fix: dont nag maintainers when nothing needs to be done (#2418) --- .github/workflows/update-submodules.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/update-submodules.yml b/.github/workflows/update-submodules.yml index 686974ad5..af597fc58 100644 --- a/.github/workflows/update-submodules.yml +++ b/.github/workflows/update-submodules.yml @@ -41,6 +41,7 @@ jobs: git config --global user.email noreply@github.com git config --global pull.rebase false - name: Update submodule + id: update-submodule run: | git checkout -b ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} git reset --hard origin/${{ matrix.branches }} @@ -49,9 +50,19 @@ jobs: git reset --hard origin/${{ matrix.submodule.branch }} cd ../.. git add ${{ matrix.submodule.path }} + + if [[ -z "$(git status --porcelain --untracked-files=no)" ]]; then + echo "No changes detected!" + echo "SUBMIT_PR=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + git commit --message "Update ${{ matrix.submodule.name }}" git push origin ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --force + echo "SUBMIT_PR=true" >> "$GITHUB_OUTPUT" + - name: Create PR + if: ${{ steps.update-submodule.outputs.SUBMIT_PR == 'true' }} run: | echo ${{ secrets.UPDATE_PAT }} | gh auth login --with-token prNumber=$(gh pr list --base ${{ matrix.branches }} --head ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --state open --json number --template "{{range .}}{{.number}}{{end}}") From 2a65d1e045ed102c141040110f303a6ee4a51e1a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 7 Nov 2025 16:04:13 +0100 Subject: [PATCH 116/477] Fix KeyNotFoundException in DtrBar.RemoveNode --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index bd8d8e1d7..920978b00 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -546,9 +546,12 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar { var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - - this.eventHandles[node->AtkResNode.NodeId].ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); - this.eventHandles[node->AtkResNode.NodeId].Clear(); + + if (this.eventHandles.TryGetValue(node->AtkResNode.NodeId, out var eventHandles)) + { + eventHandles.ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); + eventHandles.Clear(); + } var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; From 8fd49f261ade0e811e02bb8002cbfe17d3f68ff9 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 8 Nov 2025 11:31:10 +0100 Subject: [PATCH 117/477] Unify agent update events into AgentUpdate --- Dalamud/Game/Gui/AgentUpdateFlag.cs | 34 +++++++++++++++ Dalamud/Game/Gui/GameGui.cs | 55 +++---------------------- Dalamud/Game/Inventory/GameInventory.cs | 6 +-- Dalamud/Plugin/Services/IGameGui.cs | 21 ++-------- 4 files changed, 46 insertions(+), 70 deletions(-) create mode 100644 Dalamud/Game/Gui/AgentUpdateFlag.cs diff --git a/Dalamud/Game/Gui/AgentUpdateFlag.cs b/Dalamud/Game/Gui/AgentUpdateFlag.cs new file mode 100644 index 000000000..1c99e104c --- /dev/null +++ b/Dalamud/Game/Gui/AgentUpdateFlag.cs @@ -0,0 +1,34 @@ +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +namespace Dalamud.Game.Gui; + +/// +/// Represents a flag set by the game used by agents to conditionally update their addons. +/// +[Flags] +public enum AgentUpdateFlag : byte +{ + /// Set when an inventory has been updated. + InventoryUpdate = 1 << 0, + + /// Set when a hotbar slot has been executed, or a Gearset or Macro has been changed. + ActionBarUpdate = 1 << 1, + + /// Set when the RetainerMarket inventory has been updated. + RetainerMarketInventoryUpdate = 1 << 2, + + /// Unknown use case. + NameplateUpdate = 1 << 3, + + /// Set when the player unlocked collectibles, contents or systems. + UnlocksUpdate = 1 << 4, + + /// Set when was called. + MainCommandEnabledStateUpdate = 1 << 5, + + /// Set when any housing inventory has been updated. + HousingInventoryUpdate = 1 << 6, + + /// Set when any content inventory has been updated. + ContentInventoryUpdate = 1 << 7, +} diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 51ab6b1d2..1d887055e 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -95,16 +95,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui public event EventHandler? HoveredActionChanged; /// - public event Action InventoryUpdate; - - /// - public event Action ActionBarUpdate; - - /// - public event Action UnlocksUpdate; - - /// - public event Action MainCommandEnabledStateUpdate; + public event Action AgentUpdate; /// public bool GameUiHidden { get; private set; } @@ -393,20 +384,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui if (agentUpdateFlag != RaptureAtkModule.AgentUpdateFlags.None) { - if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.InventoryUpdate) || - agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.RetainerMarketInventoryUpdate) || - agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.HousingInventoryUpdate) || - agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.ContentInventoryUpdate)) - this.InventoryUpdate.InvokeSafely(); - - if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.ActionBarUpdate)) - this.ActionBarUpdate.InvokeSafely(); - - if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.UnlocksUpdate)) - this.UnlocksUpdate.InvokeSafely(); - - if (agentUpdateFlag.HasFlag(RaptureAtkModule.AgentUpdateFlags.MainCommandEnabledStateUpdate)) - this.MainCommandEnabledStateUpdate.InvokeSafely(); + this.AgentUpdate.InvokeSafely((AgentUpdateFlag)agentUpdateFlag); } } } @@ -432,10 +410,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui this.gameGuiService.UiHideToggled += this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged += this.HoveredItemForward; this.gameGuiService.HoveredActionChanged += this.HoveredActionForward; - this.gameGuiService.InventoryUpdate += this.InventoryUpdateForward; - this.gameGuiService.ActionBarUpdate += this.ActionBarUpdateForward; - this.gameGuiService.UnlocksUpdate += this.UnlocksUpdateForward; - this.gameGuiService.MainCommandEnabledStateUpdate += this.MainCommandEnabledStateUpdateForward; + this.gameGuiService.AgentUpdate += this.AgentUpdateForward; } /// @@ -448,16 +423,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui public event EventHandler? HoveredActionChanged; /// - public event Action InventoryUpdate; - - /// - public event Action ActionBarUpdate; - - /// - public event Action UnlocksUpdate; - - /// - public event Action MainCommandEnabledStateUpdate; + public event Action AgentUpdate; /// public bool GameUiHidden => this.gameGuiService.GameUiHidden; @@ -478,10 +444,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; this.gameGuiService.HoveredActionChanged -= this.HoveredActionForward; - this.gameGuiService.InventoryUpdate -= this.InventoryUpdateForward; - this.gameGuiService.ActionBarUpdate -= this.ActionBarUpdateForward; - this.gameGuiService.UnlocksUpdate -= this.UnlocksUpdateForward; - this.gameGuiService.MainCommandEnabledStateUpdate -= this.MainCommandEnabledStateUpdateForward; + this.gameGuiService.AgentUpdate -= this.AgentUpdateForward; this.UiHideToggled = null; this.HoveredItemChanged = null; @@ -534,11 +497,5 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui private void HoveredActionForward(object sender, HoveredAction hoverAction) => this.HoveredActionChanged?.Invoke(sender, hoverAction); - private void InventoryUpdateForward() => this.InventoryUpdate.InvokeSafely(); - - private void ActionBarUpdateForward() => this.ActionBarUpdate.InvokeSafely(); - - private void UnlocksUpdateForward() => this.UnlocksUpdate.InvokeSafely(); - - private void MainCommandEnabledStateUpdateForward() => this.MainCommandEnabledStateUpdate.InvokeSafely(); + private void AgentUpdateForward(AgentUpdateFlag agentUpdateFlag) => this.AgentUpdate.InvokeSafely(agentUpdateFlag); } diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index e2e3c1ec2..535b84372 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -46,7 +46,7 @@ internal class GameInventory : IInternalDisposableService this.inventoryTypes = Enum.GetValues(); this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; - this.gameGui.InventoryUpdate += this.OnInventoryUpdate; + this.gameGui.AgentUpdate += this.OnAgentUpdate; } /// @@ -58,7 +58,7 @@ internal class GameInventory : IInternalDisposableService this.subscribersPendingChange.Clear(); this.subscribersChanged = false; this.framework.Update -= this.OnFrameworkUpdate; - this.gameGui.InventoryUpdate -= this.OnInventoryUpdate; + this.gameGui.AgentUpdate -= this.OnAgentUpdate; } } @@ -309,7 +309,7 @@ internal class GameInventory : IInternalDisposableService return items; } - private void OnInventoryUpdate() + private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag) { this.inventoriesMightBeChanged |= true; } diff --git a/Dalamud/Plugin/Services/IGameGui.cs b/Dalamud/Plugin/Services/IGameGui.cs index fc37c14b7..6c2e0083e 100644 --- a/Dalamud/Plugin/Services/IGameGui.cs +++ b/Dalamud/Plugin/Services/IGameGui.cs @@ -27,25 +27,10 @@ public unsafe interface IGameGui public event EventHandler HoveredActionChanged; /// - /// Event that is fired when the inventory has been updated. + /// Fired when the game sets one or more values, + /// used by agents to conditionally update their addons. /// - event Action InventoryUpdate; - - /// - /// Fired when the action bar needs to be updated, e.g. after changing Class/Job, - /// updating Gear Sets, modifying Macros, or executing a hotbar slot. - /// - event Action ActionBarUpdate; - - /// - /// Event that is fired when collectibles, content or systems were unlocked. - /// - event Action UnlocksUpdate; - - /// - /// Event that is fired when the enable state of MainCommands has been updated. - /// - event Action MainCommandEnabledStateUpdate; + event Action AgentUpdate; /// /// Gets a value indicating whether the game UI is hidden. From 494d9a04fa548340d0de7af29c965e167d59a4e2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 8 Nov 2025 11:32:34 +0100 Subject: [PATCH 118/477] Remove unknown NameplateUpdate flag for now --- Dalamud/Game/Gui/AgentUpdateFlag.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/AgentUpdateFlag.cs b/Dalamud/Game/Gui/AgentUpdateFlag.cs index 1c99e104c..3e2808adb 100644 --- a/Dalamud/Game/Gui/AgentUpdateFlag.cs +++ b/Dalamud/Game/Gui/AgentUpdateFlag.cs @@ -17,8 +17,8 @@ public enum AgentUpdateFlag : byte /// Set when the RetainerMarket inventory has been updated. RetainerMarketInventoryUpdate = 1 << 2, - /// Unknown use case. - NameplateUpdate = 1 << 3, + // /// Unknown use case. + // NameplateUpdate = 1 << 3, /// Set when the player unlocked collectibles, contents or systems. UnlocksUpdate = 1 << 4, From 6ade5b21cfad0d951ca92471906a1d21aca23c01 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 1 Oct 2025 17:06:06 +0200 Subject: [PATCH 119/477] Add IUnlockState service --- Dalamud/Game/ItemActionType.cs | 76 +++ Dalamud/Game/UnlockState/UnlockState.cs | 798 ++++++++++++++++++++++++ Dalamud/Plugin/Services/IUnlockState.cs | 297 +++++++++ 3 files changed, 1171 insertions(+) create mode 100644 Dalamud/Game/ItemActionType.cs create mode 100644 Dalamud/Game/UnlockState/UnlockState.cs create mode 100644 Dalamud/Plugin/Services/IUnlockState.cs diff --git a/Dalamud/Game/ItemActionType.cs b/Dalamud/Game/ItemActionType.cs new file mode 100644 index 000000000..3f2ac5f17 --- /dev/null +++ b/Dalamud/Game/ItemActionType.cs @@ -0,0 +1,76 @@ +using Lumina.Excel.Sheets; + +namespace Dalamud.Game; + +/// +/// Enum for . +/// +public enum ItemActionType : ushort +{ + /// + /// Used to unlock a companion (minion). + /// + Companion = 853, + + /// + /// Used to unlock a chocobo companion barding. + /// + BuddyEquip = 1013, + + /// + /// Used to unlock a mount. + /// + Mount = 1322, + + /// + /// Used to unlock recipes from a crafting recipe book. + /// + SecretRecipeBook = 2136, + + /// + /// Used to unlock various types of content (e.g. Riding Maps, Blue Mage Totems, Emotes, Hairstyles). + /// + UnlockLink = 2633, + + /// + /// Used to unlock a Triple Triad Card. + /// + TripleTriadCard = 3357, + + /// + /// Used to unlock gathering nodes of a Folklore Tome. + /// + FolkloreTome = 4107, + + /// + /// Used to unlock an Orchestrion Roll. + /// + OrchestrionRoll = 25183, + + /// + /// Used to unlock portrait designs. + /// + FramersKit = 29459, + + /// + /// Used to unlock Bozjan Field Notes. These are server-side but are cached client-side. + /// + FieldNotes = 19743, + + /// + /// Used to unlock an Ornament (fashion accessory). + /// + Ornament = 20086, + + /// + /// Used to unlock glasses. + /// + Glasses = 37312, + + /// + /// Used for Company Seal Vouchers, which convert the item into Company Seals when used.
+ /// Can be used only if in a Grand Company.
+ /// IsUnlocked always returns false. + ///
+ CompanySealVouchers = 41120, +} diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs new file mode 100644 index 000000000..c84d1e73c --- /dev/null +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -0,0 +1,798 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +using Dalamud.Data; +using Dalamud.Game.Gui; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Component.Exd; + +using Lumina.Excel; +using Lumina.Excel.Sheets; + +using ActionSheet = Lumina.Excel.Sheets.Action; +using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent; +using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; + +namespace Dalamud.Game.UnlockState; + +/// +/// This class provides unlock state of various content in the game. +/// +[ServiceManager.EarlyLoadedService] +internal unsafe class UnlockState : IInternalDisposableService, IUnlockState +{ + private static readonly ModuleLog Log = new(nameof(UnlockState)); + + private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; + + [ServiceManager.ServiceDependency] + private readonly DataManager dataManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ClientState.ClientState clientState = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly GameGui gameGui = Service.Get(); + + [ServiceManager.ServiceConstructor] + private UnlockState() + { + this.clientState.Login += this.UpdateUnlocks; + this.clientState.Logout += this.OnLogout; + this.gameGui.UnlocksUpdate += this.UpdateUnlocks; + } + + /// + public event IUnlockState.UnlockDelegate Unlock; + + private bool IsLoaded => PlayerState.Instance()->IsLoaded; + + /// + void IInternalDisposableService.DisposeService() + { + this.clientState.Login -= this.UpdateUnlocks; + this.clientState.Logout -= this.OnLogout; + this.gameGui.UnlocksUpdate -= this.UpdateUnlocks; + } + + /// + public bool IsActionUnlocked(ActionSheet row) + { + return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId); + } + + /// + public bool IsAetherCurrentUnlocked(AetherCurrent row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsAetherCurrentUnlocked(row.RowId); + } + + /// + public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsAetherCurrentZoneComplete(row.RowId); + } + + /// + public bool IsAozActionUnlocked(AozAction row) + { + if (!this.IsLoaded) + return false; + + if (row.RowId == 0 || !row.Action.IsValid) + return false; + + return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(row.Action.Value.UnlockLink.RowId); + } + + /// + public bool IsBannerBgUnlocked(BannerBg row) + { + return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value); + } + + /// + public bool IsBannerConditionUnlocked(BannerCondition row) + { + if (row.RowId == 0) + return false; + + if (!this.IsLoaded) + return false; + + var rowPtr = ExdModule.GetBannerConditionByIndex(row.RowId); + if (rowPtr == null) + return false; + + return ExdModule.GetBannerConditionUnlockState(rowPtr) == 0; + } + + /// + public bool IsBannerDecorationUnlocked(BannerDecoration row) + { + return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value); + } + + /// + public bool IsBannerFacialUnlocked(BannerFacial row) + { + return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value); + } + + /// + public bool IsBannerFrameUnlocked(BannerFrame row) + { + return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value); + } + + /// + public bool IsBannerTimelineUnlocked(BannerTimeline row) + { + return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value); + } + + /// + public bool IsBuddyActionUnlocked(BuddyAction row) + { + return this.IsUnlockLinkUnlocked(row.UnlockLink); + } + + /// + public bool IsBuddyEquipUnlocked(BuddyEquip row) + { + if (!this.IsLoaded) + return false; + + return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.RowId); + } + + /// + public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row) + { + return row.IsPurchasable && this.IsUnlockLinkUnlocked(row.UnlockLink); + } + + /// + public bool IsChocoboTaxiUnlocked(ChocoboTaxi row) + { + if (!this.IsLoaded) + return false; + + return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId); + } + + /// + public bool IsCompanionUnlocked(Companion row) + { + if (!this.IsLoaded) + return false; + + return UIState.Instance()->IsCompanionUnlocked(row.RowId); + } + + /// + public bool IsCraftActionUnlocked(CraftAction row) + { + return this.IsUnlockLinkUnlocked(row.QuestRequirement.RowId); + } + + /// + public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row) + { + return this.IsUnlockLinkUnlocked(row.UnlockLink); + } + + /// + public bool IsEmoteUnlocked(Emote row) + { + return this.IsUnlockLinkUnlocked(row.UnlockLink); + } + + /// + public bool IsGeneralActionUnlocked(GeneralAction row) + { + return this.IsUnlockLinkUnlocked(row.UnlockLink); + } + + /// + public bool IsGlassesUnlocked(Glasses row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.RowId); + } + + /// + public bool IsHowToUnlocked(HowTo row) + { + if (!this.IsLoaded) + return false; + + return UIState.Instance()->IsHowToUnlocked(row.RowId); + } + + /// + public bool IsInstanceContentUnlocked(InstanceContentSheet row) + { + if (!this.IsLoaded) + return false; + + return UIState.IsInstanceContentUnlocked(row.RowId); + } + + /// + public unsafe bool IsItemUnlocked(Item row) + { + if (row.ItemAction.RowId == 0) + return false; + + if (!this.IsLoaded) + return false; + + // To avoid the ExdModule.GetItemRowById call, which can return null if the excel page + // is not loaded, we're going to imitate the IsItemActionUnlocked call first: + switch ((ItemActionType)row.ItemAction.Value.Type) + { + case ItemActionType.Companion: + return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]); + + case ItemActionType.BuddyEquip: + return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]); + + case ItemActionType.Mount: + return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]); + + case ItemActionType.SecretRecipeBook: + return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]); + + case ItemActionType.UnlockLink: + return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]); + + case ItemActionType.TripleTriadCard when row.AdditionalData.Is(): + return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId); + + case ItemActionType.FolkloreTome: + return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]); + + case ItemActionType.OrchestrionRoll when row.AdditionalData.Is(): + return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId); + + case ItemActionType.FramersKit: + return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId); + + case ItemActionType.Ornament: + return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]); + + case ItemActionType.Glasses: + return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId); + + case ItemActionType.CompanySealVouchers: + return false; + } + + var nativeRow = ExdModule.GetItemRowById(row.RowId); + return nativeRow != null && UIState.Instance()->IsItemActionUnlocked(nativeRow) == 1; + } + + /// + public bool IsMcGuffinUnlocked(McGuffin row) + { + return PlayerState.Instance()->IsMcGuffinUnlocked(row.RowId); + } + + /// + public bool IsMJILandmarkUnlocked(MJILandmark row) + { + return this.IsUnlockLinkUnlocked(row.UnlockLink); + } + + /// + public bool IsMountUnlocked(Mount row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsMountUnlocked(row.RowId); + } + + /// + public bool IsNotebookDivisionUnlocked(NotebookDivision row) + { + return this.IsUnlockLinkUnlocked(row.QuestUnlock.RowId); + } + + /// + public bool IsOrchestrionUnlocked(Orchestrion row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.RowId); + } + + /// + public bool IsOrnamentUnlocked(Ornament row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsOrnamentUnlocked(row.RowId); + } + + /// + public bool IsPerformUnlocked(Perform row) + { + return this.IsUnlockLinkUnlocked((uint)row.UnlockLink); + } + + /// + public bool IsPublicContentUnlocked(PublicContentSheet row) + { + if (!this.IsLoaded) + return false; + + return UIState.IsPublicContentUnlocked(row.RowId); + } + + /// + public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId); + } + + /// + public bool IsTraitUnlocked(Trait row) + { + return this.IsUnlockLinkUnlocked(row.Quest.RowId); + } + + /// + public bool IsTripleTriadCardUnlocked(TripleTriadCard row) + { + if (!this.IsLoaded) + return false; + + return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.RowId); + } + + /// + public bool IsItemUnlockable(Item row) + { + if (row.ItemAction.RowId == 0) + return false; + + return (ItemActionType)row.ItemAction.Value.Type is + ItemActionType.Companion or + ItemActionType.BuddyEquip or + ItemActionType.Mount or + ItemActionType.SecretRecipeBook or + ItemActionType.UnlockLink or + ItemActionType.TripleTriadCard or + ItemActionType.FolkloreTome or + ItemActionType.OrchestrionRoll or + ItemActionType.FramersKit or + ItemActionType.Ornament or + ItemActionType.Glasses; + } + + /// + public bool IsRowRefUnlocked(RowRef rowRef) where T : struct, IExcelRow + { + return this.IsRowRefUnlocked((RowRef)rowRef); + } + + /// + public bool IsRowRefUnlocked(RowRef rowRef) + { + if (!this.IsLoaded || rowRef.IsUntyped) + return false; + + if (rowRef.TryGetValue(out var actionRow)) + return this.IsActionUnlocked(actionRow); + + if (rowRef.TryGetValue(out var aetherCurrentRow)) + return this.IsAetherCurrentUnlocked(aetherCurrentRow); + + if (rowRef.TryGetValue(out var aetherCurrentCompFlgSetRow)) + return this.IsAetherCurrentCompFlgSetUnlocked(aetherCurrentCompFlgSetRow); + + if (rowRef.TryGetValue(out var aozActionRow)) + return this.IsAozActionUnlocked(aozActionRow); + + if (rowRef.TryGetValue(out var bannerBgRow)) + return this.IsBannerBgUnlocked(bannerBgRow); + + if (rowRef.TryGetValue(out var bannerConditionRow)) + return this.IsBannerConditionUnlocked(bannerConditionRow); + + if (rowRef.TryGetValue(out var bannerDecorationRow)) + return this.IsBannerDecorationUnlocked(bannerDecorationRow); + + if (rowRef.TryGetValue(out var bannerFacialRow)) + return this.IsBannerFacialUnlocked(bannerFacialRow); + + if (rowRef.TryGetValue(out var bannerFrameRow)) + return this.IsBannerFrameUnlocked(bannerFrameRow); + + if (rowRef.TryGetValue(out var bannerTimelineRow)) + return this.IsBannerTimelineUnlocked(bannerTimelineRow); + + if (rowRef.TryGetValue(out var buddyActionRow)) + return this.IsBuddyActionUnlocked(buddyActionRow); + + if (rowRef.TryGetValue(out var buddyEquipRow)) + return this.IsBuddyEquipUnlocked(buddyEquipRow); + + if (rowRef.TryGetValue(out var csBonusContentTypeRow)) + return this.IsCSBonusContentTypeUnlocked(csBonusContentTypeRow); + + if (rowRef.TryGetValue(out var charaMakeCustomizeRow)) + return this.IsCharaMakeCustomizeUnlocked(charaMakeCustomizeRow); + + if (rowRef.TryGetValue(out var chocoboTaxiRow)) + return this.IsChocoboTaxiUnlocked(chocoboTaxiRow); + + if (rowRef.TryGetValue(out var companionRow)) + return this.IsCompanionUnlocked(companionRow); + + if (rowRef.TryGetValue(out var craftActionRow)) + return this.IsCraftActionUnlocked(craftActionRow); + + if (rowRef.TryGetValue(out var emoteRow)) + return this.IsEmoteUnlocked(emoteRow); + + if (rowRef.TryGetValue(out var generalActionRow)) + return this.IsGeneralActionUnlocked(generalActionRow); + + if (rowRef.TryGetValue(out var glassesRow)) + return this.IsGlassesUnlocked(glassesRow); + + if (rowRef.TryGetValue(out var howToRow)) + return this.IsHowToUnlocked(howToRow); + + if (rowRef.TryGetValue(out var instanceContentRow)) + return this.IsInstanceContentUnlocked(instanceContentRow); + + if (rowRef.TryGetValue(out var itemRow)) + return this.IsItemUnlocked(itemRow); + + if (rowRef.TryGetValue(out var mjiLandmarkRow)) + return this.IsMJILandmarkUnlocked(mjiLandmarkRow); + + if (rowRef.TryGetValue(out var mcGuffinRow)) + return this.IsMcGuffinUnlocked(mcGuffinRow); + + if (rowRef.TryGetValue(out var mountRow)) + return this.IsMountUnlocked(mountRow); + + if (rowRef.TryGetValue(out var notebookDivisionRow)) + return this.IsNotebookDivisionUnlocked(notebookDivisionRow); + + if (rowRef.TryGetValue(out var orchestrionRow)) + return this.IsOrchestrionUnlocked(orchestrionRow); + + if (rowRef.TryGetValue(out var ornamentRow)) + return this.IsOrnamentUnlocked(ornamentRow); + + if (rowRef.TryGetValue(out var performRow)) + return this.IsPerformUnlocked(performRow); + + if (rowRef.TryGetValue(out var publicContentRow)) + return this.IsPublicContentUnlocked(publicContentRow); + + if (rowRef.TryGetValue(out var secretRecipeBookRow)) + return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow); + + if (rowRef.TryGetValue(out var traitRow)) + return this.IsTraitUnlocked(traitRow); + + if (rowRef.TryGetValue(out var tripleTriadCardRow)) + return this.IsTripleTriadCardUnlocked(tripleTriadCardRow); + + return false; + } + + /// + public bool IsUnlockLinkUnlocked(ushort unlockLink) + { + if (!this.IsLoaded) + return false; + + if (unlockLink == 0) + return false; + + return UIState.Instance()->IsUnlockLinkUnlocked(unlockLink); + } + + /// + public bool IsUnlockLinkUnlocked(uint unlockLink) + { + if (!this.IsLoaded) + return false; + + if (unlockLink == 0) + return false; + + return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(unlockLink); + } + + private void UpdateUnlocks() + { + try + { + this.UpdateUnlocks(false); + } + catch (Exception ex) + { + Log.Error(ex, "Error during initial unlock check"); + } + } + + private void OnLogout(int type, int code) + { + this.cachedUnlockedRowIds.Clear(); + } + + private void UpdateUnlocks(bool fireEvent) + { + if (!this.IsLoaded) + return; + + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + + // Not implemented: + // - DescriptionPage: quite complex + // - QuestAcceptAdditionCondition: ignored + + // For some other day: + // - FishingSpot + // - Spearfishing + // - Adventure (Sightseeing) + // - Recipes + // - MinerFolkloreTome + // - BotanistFolkloreTome + // - FishingFolkloreTome + // - VVD or is that unlocked via quest? + // - VVDNotebookContents? + // - FramersKit (is that just an Item?) + // - ... more? + + // Probably not happening, because it requires fetching data from server: + // - Achievements + // - Titles + // - Bozjan Field Notes + } + + private void UpdateUnlocksForSheet(bool fireEvent = true) where T : struct, IExcelRow + { + var unlockedRowIds = this.cachedUnlockedRowIds.GetOrAdd(typeof(T), _ => []); + + foreach (var row in this.dataManager.GetExcelSheet()) + { + if (unlockedRowIds.Contains(row.RowId)) + continue; + + var rowRef = LuminaUtils.CreateRef(row.RowId); + + if (!this.IsRowRefUnlocked(rowRef)) + continue; + + unlockedRowIds.Add(row.RowId); + + if (fireEvent) + { + Log.Verbose("Unlock detected: {row}", $"{typeof(T).Name}#{row.RowId}"); + + foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + { + try + { + action((RowRef)rowRef); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); + } + } + } + } + } +} + +/// +/// Plugin-scoped version of a service. +/// +[PluginInterface] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockState +{ + [ServiceManager.ServiceDependency] + private readonly UnlockState unlockStateService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal UnlockStatePluginScoped() + { + this.unlockStateService.Unlock += this.UnlockForward; + } + + /// + public event IUnlockState.UnlockDelegate? Unlock; + + /// + public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row); + + /// + public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row); + + /// + public bool IsAetherCurrentUnlocked(AetherCurrent row) => this.unlockStateService.IsAetherCurrentUnlocked(row); + + /// + public bool IsAozActionUnlocked(AozAction row) => this.unlockStateService.IsAozActionUnlocked(row); + + /// + public bool IsBannerBgUnlocked(BannerBg row) => this.unlockStateService.IsBannerBgUnlocked(row); + + /// + public bool IsBannerConditionUnlocked(BannerCondition row) => this.unlockStateService.IsBannerConditionUnlocked(row); + + /// + public bool IsBannerDecorationUnlocked(BannerDecoration row) => this.unlockStateService.IsBannerDecorationUnlocked(row); + + /// + public bool IsBannerFacialUnlocked(BannerFacial row) => this.unlockStateService.IsBannerFacialUnlocked(row); + + /// + public bool IsBannerFrameUnlocked(BannerFrame row) => this.unlockStateService.IsBannerFrameUnlocked(row); + + /// + public bool IsBannerTimelineUnlocked(BannerTimeline row) => this.unlockStateService.IsBannerTimelineUnlocked(row); + + /// + public bool IsBuddyActionUnlocked(BuddyAction row) => this.unlockStateService.IsBuddyActionUnlocked(row); + + /// + public bool IsBuddyEquipUnlocked(BuddyEquip row) => this.unlockStateService.IsBuddyEquipUnlocked(row); + + /// + public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row) => this.unlockStateService.IsCharaMakeCustomizeUnlocked(row); + + /// + public bool IsChocoboTaxiUnlocked(ChocoboTaxi row) => this.unlockStateService.IsChocoboTaxiUnlocked(row); + + /// + public bool IsCompanionUnlocked(Companion row) => this.unlockStateService.IsCompanionUnlocked(row); + + /// + public bool IsCraftActionUnlocked(CraftAction row) => this.unlockStateService.IsCraftActionUnlocked(row); + + /// + public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row) => this.unlockStateService.IsCSBonusContentTypeUnlocked(row); + + /// + public bool IsEmoteUnlocked(Emote row) => this.unlockStateService.IsEmoteUnlocked(row); + + /// + public bool IsGeneralActionUnlocked(GeneralAction row) => this.unlockStateService.IsGeneralActionUnlocked(row); + + /// + public bool IsGlassesUnlocked(Glasses row) => this.unlockStateService.IsGlassesUnlocked(row); + + /// + public bool IsHowToUnlocked(HowTo row) => this.unlockStateService.IsHowToUnlocked(row); + + /// + public bool IsInstanceContentUnlocked(InstanceContentSheet row) => this.unlockStateService.IsInstanceContentUnlocked(row); + + /// + public bool IsItemUnlockable(Item row) => this.unlockStateService.IsItemUnlockable(row); + + /// + public bool IsItemUnlocked(Item row) => this.unlockStateService.IsItemUnlocked(row); + + /// + public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row); + + /// + public bool IsMJILandmarkUnlocked(MJILandmark row) => this.unlockStateService.IsMJILandmarkUnlocked(row); + + /// + public bool IsMountUnlocked(Mount row) => this.unlockStateService.IsMountUnlocked(row); + + /// + public bool IsNotebookDivisionUnlocked(NotebookDivision row) => this.unlockStateService.IsNotebookDivisionUnlocked(row); + + /// + public bool IsOrchestrionUnlocked(Orchestrion row) => this.unlockStateService.IsOrchestrionUnlocked(row); + + /// + public bool IsOrnamentUnlocked(Ornament row) => this.unlockStateService.IsOrnamentUnlocked(row); + + /// + public bool IsPerformUnlocked(Perform row) => this.unlockStateService.IsPerformUnlocked(row); + + /// + public bool IsPublicContentUnlocked(PublicContentSheet row) => this.unlockStateService.IsPublicContentUnlocked(row); + + /// + public bool IsRowRefUnlocked(RowRef rowRef) => this.unlockStateService.IsRowRefUnlocked(rowRef); + + /// + public bool IsRowRefUnlocked(RowRef rowRef) where T : struct, IExcelRow => this.unlockStateService.IsRowRefUnlocked(rowRef); + + /// + public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row); + + /// + public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row); + + /// + public bool IsTripleTriadCardUnlocked(TripleTriadCard row) => this.unlockStateService.IsTripleTriadCardUnlocked(row); + + /// + public bool IsUnlockLinkUnlocked(uint unlockLink) => this.unlockStateService.IsUnlockLinkUnlocked(unlockLink); + + /// + public bool IsUnlockLinkUnlocked(ushort unlockLink) => this.unlockStateService.IsUnlockLinkUnlocked(unlockLink); + + /// + void IInternalDisposableService.DisposeService() + { + this.unlockStateService.Unlock -= this.UnlockForward; + } + + private void UnlockForward(RowRef rowRef) => this.Unlock?.Invoke(rowRef); +} diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs new file mode 100644 index 000000000..baee47115 --- /dev/null +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -0,0 +1,297 @@ +using Lumina.Excel; +using Lumina.Excel.Sheets; + +namespace Dalamud.Plugin.Services; + +#pragma warning disable SA1400 // Access modifier should be declared: Interface members are public by default + +/// +/// Interface for determining unlock state of various content in the game. +/// +public interface IUnlockState +{ + /// + /// A delegate type used for the event. + /// + /// A RowRef of the unlocked thing. + delegate void UnlockDelegate(RowRef rowRef); + + /// + /// Event triggered when something was unlocked. + /// + event UnlockDelegate? Unlock; + + /// + /// Determines whether the specified Action is unlocked. + /// + /// The Action row to check. + /// if unlocked; otherwise, . + bool IsActionUnlocked(Lumina.Excel.Sheets.Action row); + + /// + /// Determines whether the specified AetherCurrentCompFlgSet is unlocked. + /// + /// The AetherCurrentCompFlgSet row to check. + /// if unlocked; otherwise, . + bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row); + + /// + /// Determines whether the specified AetherCurrent is unlocked. + /// + /// The AetherCurrent row to check. + /// if unlocked; otherwise, . + bool IsAetherCurrentUnlocked(AetherCurrent row); + + /// + /// Determines whether the specified AozAction (Blue Mage Action) is unlocked. + /// + /// The AozAction row to check. + /// if unlocked; otherwise, . + bool IsAozActionUnlocked(AozAction row); + + /// + /// Determines whether the specified BannerBg (Portrait Backgrounds) is unlocked. + /// + /// The BannerBg row to check. + /// if unlocked; otherwise, . + bool IsBannerBgUnlocked(BannerBg row); + + /// + /// Determines whether the specified BannerCondition is unlocked. + /// + /// The BannerCondition row to check. + /// if unlocked; otherwise, . + bool IsBannerConditionUnlocked(BannerCondition row); + + /// + /// Determines whether the specified BannerDecoration (Portrait Accents) is unlocked. + /// + /// The BannerDecoration row to check. + /// if unlocked; otherwise, . + bool IsBannerDecorationUnlocked(BannerDecoration row); + + /// + /// Determines whether the specified BannerFacial (Portrait Expressions) is unlocked. + /// + /// The BannerFacial row to check. + /// if unlocked; otherwise, . + bool IsBannerFacialUnlocked(BannerFacial row); + + /// + /// Determines whether the specified BannerFrame (Portrait Frames) is unlocked. + /// + /// The BannerFrame row to check. + /// if unlocked; otherwise, . + bool IsBannerFrameUnlocked(BannerFrame row); + + /// + /// Determines whether the specified BannerTimeline (Portrait Poses) is unlocked. + /// + /// The BannerTimeline row to check. + /// if unlocked; otherwise, . + bool IsBannerTimelineUnlocked(BannerTimeline row); + + /// + /// Determines whether the specified BuddyAction (Action of the players Chocobo Companion) is unlocked. + /// + /// The BuddyAction row to check. + /// if unlocked; otherwise, . + bool IsBuddyActionUnlocked(BuddyAction row); + + /// + /// Determines whether the specified BuddyEquip (Equipment of the players Chocobo Companion) is unlocked. + /// + /// The BuddyEquip row to check. + /// if unlocked; otherwise, . + bool IsBuddyEquipUnlocked(BuddyEquip row); + + /// + /// Determines whether the specified CharaMakeCustomize (Hairstyles and Face Paint patterns) is unlocked. + /// + /// The CharaMakeCustomize row to check. + /// if unlocked; otherwise, . + bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row); + + /// + /// Determines whether the specified ChocoboTaxi (Chocobokeeps of the Chocobo Porter service) is unlocked. + /// + /// The ChocoboTaxi row to check. + /// if unlocked; otherwise, . + bool IsChocoboTaxiUnlocked(ChocoboTaxi row); + + /// + /// Determines whether the specified Companion (Minions) is unlocked. + /// + /// The Companion row to check. + /// if unlocked; otherwise, . + bool IsCompanionUnlocked(Companion row); + + /// + /// Determines whether the specified CraftAction is unlocked. + /// + /// The CraftAction row to check. + /// if unlocked; otherwise, . + bool IsCraftActionUnlocked(CraftAction row); + + /// + /// Determines whether the specified CSBonusContentType is unlocked. + /// + /// The CSBonusContentType row to check. + /// if unlocked; otherwise, . + bool IsCSBonusContentTypeUnlocked(CSBonusContentType row); + + /// + /// Determines whether the specified Emote is unlocked. + /// + /// The Emote row to check. + /// if unlocked; otherwise, . + bool IsEmoteUnlocked(Emote row); + + /// + /// Determines whether the specified GeneralAction is unlocked. + /// + /// The GeneralAction row to check. + /// if unlocked; otherwise, . + bool IsGeneralActionUnlocked(GeneralAction row); + + /// + /// Determines whether the specified Glasses is unlocked. + /// + /// The Glasses row to check. + /// if unlocked; otherwise, . + bool IsGlassesUnlocked(Glasses row); + + /// + /// Determines whether the specified HowTo is unlocked. + /// + /// The HowTo row to check. + /// if unlocked; otherwise, . + bool IsHowToUnlocked(HowTo row); + + /// + /// Determines whether the specified InstanceContent is unlocked. + /// + /// The InstanceContent row to check. + /// if unlocked; otherwise, . + bool IsInstanceContentUnlocked(InstanceContent row); + + /// + /// Determines whether the specified Item is considered unlockable. + /// + /// The Item row to check. + /// if unlockable; otherwise, . + bool IsItemUnlockable(Item row); + + /// + /// Determines whether the specified Item is unlocked. + /// + /// The Item row to check. + /// if unlocked; otherwise, . + bool IsItemUnlocked(Item row); + + /// + /// Determines whether the specified McGuffin is unlocked. + /// + /// The McGuffin row to check. + /// if unlocked; otherwise, . + bool IsMcGuffinUnlocked(McGuffin row); + + /// + /// Determines whether the specified MJILandmark (Island Sanctuary landmark) is unlocked. + /// + /// The MJILandmark row to check. + /// if unlocked; otherwise, . + bool IsMJILandmarkUnlocked(MJILandmark row); + + /// + /// Determines whether the specified Mount is unlocked. + /// + /// The Mount row to check. + /// if unlocked; otherwise, . + bool IsMountUnlocked(Mount row); + + /// + /// Determines whether the specified NotebookDivision (Categories in Crafting/Gathering Log) is unlocked. + /// + /// The NotebookDivision row to check. + /// if unlocked; otherwise, . + bool IsNotebookDivisionUnlocked(NotebookDivision row); + + /// + /// Determines whether the specified Orchestrion roll is unlocked. + /// + /// The Orchestrion row to check. + /// if unlocked; otherwise, . + bool IsOrchestrionUnlocked(Orchestrion row); + + /// + /// Determines whether the specified Ornament (Fashion Accessories) is unlocked. + /// + /// The Ornament row to check. + /// if unlocked; otherwise, . + bool IsOrnamentUnlocked(Ornament row); + + /// + /// Determines whether the specified Perform (Performance Instruments) is unlocked. + /// + /// The Perform row to check. + /// if unlocked; otherwise, . + bool IsPerformUnlocked(Perform row); + + /// + /// Determines whether the specified PublicContent is unlocked. + /// + /// The PublicContent row to check. + /// if unlocked; otherwise, . + bool IsPublicContentUnlocked(PublicContent row); + + /// + /// Determines whether the underlying RowRef type is unlocked. + /// + /// The RowRef to check. + /// if unlocked; otherwise, . + bool IsRowRefUnlocked(RowRef rowRef); + + /// + /// Determines whether the underlying RowRef type is unlocked. + /// + /// The type of the Excel row. + /// The RowRef to check. + /// if unlocked; otherwise, . + bool IsRowRefUnlocked(RowRef rowRef) where T : struct, IExcelRow; + + /// + /// Determines whether the specified SecretRecipeBook (Master Recipe Books) is unlocked. + /// + /// The SecretRecipeBook row to check. + /// if unlocked; otherwise, . + bool IsSecretRecipeBookUnlocked(SecretRecipeBook row); + + /// + /// Determines whether the specified Trait is unlocked. + /// + /// The Trait row to check. + /// if unlocked; otherwise, . + bool IsTraitUnlocked(Trait row); + + /// + /// Determines whether the specified TripleTriadCard is unlocked. + /// + /// The TripleTriadCard row to check. + /// if unlocked; otherwise, . + bool IsTripleTriadCardUnlocked(TripleTriadCard row); + + /// + /// Determines whether the specified unlock link is unlocked or quest is completed. + /// + /// The unlock link id or quest id (quest ids in this case are over 65536). + /// if unlocked; otherwise, . + bool IsUnlockLinkUnlocked(uint unlockLink); + + /// + /// Determines whether the specified unlock link is unlocked. + /// + /// The unlock link id. + /// if unlocked; otherwise, . + bool IsUnlockLinkUnlocked(ushort unlockLink); +} From ba159f8c5fd8e396535d87d361405c7c6d5564af Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 1 Oct 2025 23:59:01 +0200 Subject: [PATCH 120/477] Add IsRecipeUnlocked --- Dalamud/Game/UnlockState/RecipeData.cs | 257 ++++++++++++++++++++++++ Dalamud/Game/UnlockState/UnlockState.cs | 17 +- Dalamud/Plugin/Services/IUnlockState.cs | 7 + 3 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Game/UnlockState/RecipeData.cs diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs new file mode 100644 index 000000000..81c0b838b --- /dev/null +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -0,0 +1,257 @@ +using System.Linq; + +using CommunityToolkit.HighPerformance; + +using Dalamud.Data; +using Dalamud.Game.Gui; + +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.Interop; + +using Lumina.Excel.Sheets; + +namespace Dalamud.Game.UnlockState; + +/// +/// Represents recipe-related data for all crafting classes. +/// +[ServiceManager.EarlyLoadedService] +internal unsafe class RecipeData : IInternalDisposableService +{ + [ServiceManager.ServiceDependency] + private readonly DataManager dataManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ClientState.ClientState clientState = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly GameGui gameGui = Service.Get(); + + private readonly ushort[] craftTypeLevels; + private readonly byte[] unlockedNoteBookDivisionsCount; + private readonly byte[] unlockedSecretNoteBookDivisionsCount; + private readonly ushort[,] noteBookDivisionIds; + private byte[]? cachedUnlockedSecretRecipeBooks; + private byte[]? cachedUnlockLinks; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public RecipeData() + { + var numCraftTypes = this.dataManager.GetExcelSheet().Count(); + var numSecretNotBookDivisions = this.dataManager.GetExcelSheet().Count(row => row.RowId is >= 1000 and < 2000); + + this.unlockedNoteBookDivisionsCount = new byte[numCraftTypes]; + this.unlockedSecretNoteBookDivisionsCount = new byte[numCraftTypes]; + this.noteBookDivisionIds = new ushort[numCraftTypes, numSecretNotBookDivisions]; + + this.craftTypeLevels = new ushort[numCraftTypes]; + + this.clientState.Login += this.Update; + this.clientState.Logout += this.OnLogout; + this.gameGui.UnlocksUpdate += this.Update; + } + + /// + void IInternalDisposableService.DisposeService() + { + this.clientState.Login -= this.Update; + this.clientState.Logout -= this.OnLogout; + this.gameGui.UnlocksUpdate -= this.Update; + } + + /// + /// Determines whether the specified Recipe is unlocked. + /// + /// The Recipe row to check. + /// if unlocked; otherwise, . + public bool IsRecipeUnlocked(Recipe row) + { + // E8 ?? ?? ?? ?? 48 63 76 (2025.09.04) + var division = row.RecipeNotebookList.RowId != 0 && row.RecipeNotebookList.IsValid + ? (row.RecipeNotebookList.RowId - 1000) / 8 + 1000 + : ((uint)row.RecipeLevelTable.Value.ClassJobLevel - 1) / 5; + + // E8 ?? ?? ?? ?? 33 ED 84 C0 75 (2025.09.04) + foreach (var craftTypeRow in this.dataManager.GetExcelSheet()) + { + var craftType = (byte)craftTypeRow.RowId; + + if (division < this.unlockedNoteBookDivisionsCount[craftType]) + return true; + + if (this.unlockedNoteBookDivisionsCount[craftType] == 0) + continue; + + if (division is 5000 or 5001) + return true; + + if (division < 1000) + continue; + + if (this.unlockedSecretNoteBookDivisionsCount[craftType] == 0) + continue; + + if (this.noteBookDivisionIds.GetRowSpan(craftType).Contains((ushort)division)) + return true; + } + + return false; + } + + private void OnLogout(int type, int code) + { + this.cachedUnlockedSecretRecipeBooks = null; + this.cachedUnlockLinks = null; + } + + private void Update() + { + // Client::Game::UI::RecipeNote.InitializeStructs + + if (!this.NeedsUpdate()) + return; + + Array.Clear(this.unlockedNoteBookDivisionsCount, 0, this.unlockedNoteBookDivisionsCount.Length); + Array.Clear(this.unlockedSecretNoteBookDivisionsCount, 0, this.unlockedSecretNoteBookDivisionsCount.Length); + Array.Clear(this.noteBookDivisionIds, 0, this.noteBookDivisionIds.Length); + + foreach (var craftTypeRow in this.dataManager.GetExcelSheet()) + { + var craftType = (byte)craftTypeRow.RowId; + var craftTypeLevel = RecipeNote.Instance()->GetCraftTypeLevel(craftType); + if (craftTypeLevel == 0) + continue; + + var noteBookDivisionIndex = -1; + + foreach (var noteBookDivisionRow in this.dataManager.GetExcelSheet()) + { + if (noteBookDivisionRow.RowId < 1000) + { + if (craftTypeLevel >= noteBookDivisionRow.CraftOpeningLevel) + this.unlockedNoteBookDivisionsCount[craftType]++; + } + else if (noteBookDivisionRow.RowId < 2000) + { + noteBookDivisionIndex++; + + // For future Lumina.Excel update, replace with: + // if (!notebookDivisionRow.AllowedCraftTypes[craftType]) + // continue; + + switch (craftTypeRow.RowId) + { + case 0 when !noteBookDivisionRow.CRPCraft: continue; + case 1 when !noteBookDivisionRow.BSMCraft: continue; + case 2 when !noteBookDivisionRow.ARMCraft: continue; + case 3 when !noteBookDivisionRow.GSMCraft: continue; + case 4 when !noteBookDivisionRow.LTWCraft: continue; + case 5 when !noteBookDivisionRow.WVRCraft: continue; + case 6 when !noteBookDivisionRow.ALCCraft: continue; + case 7 when !noteBookDivisionRow.CULCraft: continue; + } + + if (noteBookDivisionRow.GatheringOpeningLevel != byte.MaxValue) + continue; + + // For future Lumina.Excel update, replace with: + // if (notebookDivisionRow.RequiresSecretRecipeBookGroupUnlock) + if (noteBookDivisionRow.Unknown1) + { + var secretRecipeBookUnlocked = false; + + // For future Lumina.Excel update, iterate over notebookDivisionRow.SecretRecipeBookGroups + for (var i = 0; i < 2; i++) + { + // For future Lumina.Excel update, replace with: + // if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid) + // continue; + var secretRecipeBookGroupRowId = i switch + { + 0 => noteBookDivisionRow.Unknown2, + 1 => noteBookDivisionRow.Unknown2, + _ => default, + }; + + if (secretRecipeBookGroupRowId == 0) + continue; + + if (!this.dataManager.GetExcelSheet().TryGetRow(secretRecipeBookGroupRowId, out var secretRecipeBookGroupRow)) + continue; + + // For future Lumina.Excel update, replace with: + // var bitIndex = secretRecipeBookGroup.Value.UnlockBitIndex[craftType]; + + var bitIndex = craftType switch + { + 0 => secretRecipeBookGroupRow.Unknown0, + 1 => secretRecipeBookGroupRow.Unknown1, + 2 => secretRecipeBookGroupRow.Unknown2, + 3 => secretRecipeBookGroupRow.Unknown3, + 4 => secretRecipeBookGroupRow.Unknown4, + 5 => secretRecipeBookGroupRow.Unknown5, + 6 => secretRecipeBookGroupRow.Unknown6, + 7 => secretRecipeBookGroupRow.Unknown7, + _ => default, + }; + + if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.TryCheckBitInSpan(bitIndex, out var result) && result) + { + secretRecipeBookUnlocked = true; + break; + } + } + + if (noteBookDivisionRow.CraftOpeningLevel > craftTypeLevel && !secretRecipeBookUnlocked) + continue; + } + else if (craftTypeLevel < noteBookDivisionRow.CraftOpeningLevel) + { + continue; + } + else if (noteBookDivisionRow.QuestUnlock.RowId != 0 && !UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(noteBookDivisionRow.QuestUnlock.RowId)) + { + continue; + } + + this.unlockedSecretNoteBookDivisionsCount[craftType]++; + this.noteBookDivisionIds[craftType, noteBookDivisionIndex] = (ushort)noteBookDivisionRow.RowId; + } + } + } + } + + private bool NeedsUpdate() + { + var changed = false; + + foreach (var craftTypeRow in this.dataManager.GetExcelSheet()) + { + var craftType = (byte)craftTypeRow.RowId; + var craftTypeLevel = RecipeNote.Instance()->GetCraftTypeLevel(craftType); + + if (this.craftTypeLevels[craftType] != craftTypeLevel) + { + this.craftTypeLevels[craftType] = craftTypeLevel; + changed |= true; + } + } + + if (this.cachedUnlockedSecretRecipeBooks == null || !PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.SequenceEqual(this.cachedUnlockedSecretRecipeBooks)) + { + this.cachedUnlockedSecretRecipeBooks = PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.ToArray(); + changed |= true; + } + + if (this.cachedUnlockLinks == null || !UIState.Instance()->UnlockLinkBitmask.SequenceEqual(this.cachedUnlockLinks)) + { + this.cachedUnlockLinks = UIState.Instance()->UnlockLinkBitmask.ToArray(); + changed |= true; + } + + return changed; + } +} diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index c84d1e73c..d41dce2e4 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -39,6 +39,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState [ServiceManager.ServiceDependency] private readonly GameGui gameGui = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly RecipeData recipeData = Service.Get(); + [ServiceManager.ServiceConstructor] private UnlockState() { @@ -346,6 +349,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return UIState.IsPublicContentUnlocked(row.RowId); } + /// + public bool IsRecipeUnlocked(Recipe row) + { + return this.recipeData.IsRecipeUnlocked(row); + } + /// public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) { @@ -495,6 +504,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (rowRef.TryGetValue(out var publicContentRow)) return this.IsPublicContentUnlocked(publicContentRow); + if (rowRef.TryGetValue(out var recipeRow)) + return this.IsRecipeUnlocked(recipeRow); + if (rowRef.TryGetValue(out var secretRecipeBookRow)) return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow); @@ -584,6 +596,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); @@ -596,7 +609,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - FishingSpot // - Spearfishing // - Adventure (Sightseeing) - // - Recipes // - MinerFolkloreTome // - BotanistFolkloreTome // - FishingFolkloreTome @@ -767,6 +779,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// public bool IsPublicContentUnlocked(PublicContentSheet row) => this.unlockStateService.IsPublicContentUnlocked(row); + /// + public bool IsRecipeUnlocked(Recipe row) => this.unlockStateService.IsRecipeUnlocked(row); + /// public bool IsRowRefUnlocked(RowRef rowRef) => this.unlockStateService.IsRowRefUnlocked(rowRef); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index baee47115..371af033c 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -245,6 +245,13 @@ public interface IUnlockState /// if unlocked; otherwise, . bool IsPublicContentUnlocked(PublicContent row); + /// + /// Determines whether the specified Recipe is unlocked. + /// + /// The Recipe row to check. + /// if unlocked; otherwise, . + bool IsRecipeUnlocked(Recipe row); + /// /// Determines whether the underlying RowRef type is unlocked. /// From 62fdd2c60d252bb5a0558de7834678869c7546a1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 2 Oct 2025 02:01:11 +0200 Subject: [PATCH 121/477] Fix IsChocoboTaxiStandUnlocked --- Dalamud/Game/UnlockState/UnlockState.cs | 10 +++++----- Dalamud/Plugin/Services/IUnlockState.cs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index d41dce2e4..ff1effdd1 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -167,12 +167,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState } /// - public bool IsChocoboTaxiUnlocked(ChocoboTaxi row) + public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row) { if (!this.IsLoaded) return false; - return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId); + return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId - 0x120000); } /// @@ -453,8 +453,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (rowRef.TryGetValue(out var charaMakeCustomizeRow)) return this.IsCharaMakeCustomizeUnlocked(charaMakeCustomizeRow); - if (rowRef.TryGetValue(out var chocoboTaxiRow)) - return this.IsChocoboTaxiUnlocked(chocoboTaxiRow); + if (rowRef.TryGetValue(out var chocoboTaxiStandRow)) + return this.IsChocoboTaxiStandUnlocked(chocoboTaxiStandRow); if (rowRef.TryGetValue(out var companionRow)) return this.IsCompanionUnlocked(companionRow); @@ -723,7 +723,7 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row) => this.unlockStateService.IsCharaMakeCustomizeUnlocked(row); /// - public bool IsChocoboTaxiUnlocked(ChocoboTaxi row) => this.unlockStateService.IsChocoboTaxiUnlocked(row); + public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row) => this.unlockStateService.IsChocoboTaxiStandUnlocked(row); /// public bool IsCompanionUnlocked(Companion row) => this.unlockStateService.IsCompanionUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 371af033c..d3620ffe2 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -113,11 +113,11 @@ public interface IUnlockState bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row); /// - /// Determines whether the specified ChocoboTaxi (Chocobokeeps of the Chocobo Porter service) is unlocked. + /// Determines whether the specified ChocoboTaxiStand (Chocobokeeps of the Chocobo Porter service) is unlocked. /// - /// The ChocoboTaxi row to check. + /// The ChocoboTaxiStand row to check. /// if unlocked; otherwise, . - bool IsChocoboTaxiUnlocked(ChocoboTaxi row); + bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row); /// /// Determines whether the specified Companion (Minions) is unlocked. From 5905afdf103b55fd2ff450579ec340722d562ca4 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 2 Oct 2025 03:23:54 +0200 Subject: [PATCH 122/477] Cache completed Quests in RecipeData too --- Dalamud/Game/UnlockState/RecipeData.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 81c0b838b..4c89c2f3e 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -5,6 +5,7 @@ using CommunityToolkit.HighPerformance; using Dalamud.Data; using Dalamud.Game.Gui; +using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.Interop; @@ -33,6 +34,7 @@ internal unsafe class RecipeData : IInternalDisposableService private readonly ushort[,] noteBookDivisionIds; private byte[]? cachedUnlockedSecretRecipeBooks; private byte[]? cachedUnlockLinks; + private byte[]? cachedCompletedQuests; /// /// Initializes a new instance of the class. @@ -105,6 +107,7 @@ internal unsafe class RecipeData : IInternalDisposableService { this.cachedUnlockedSecretRecipeBooks = null; this.cachedUnlockLinks = null; + this.cachedCompletedQuests = null; } private void Update() @@ -252,6 +255,12 @@ internal unsafe class RecipeData : IInternalDisposableService changed |= true; } + if (this.cachedCompletedQuests == null || !QuestManager.Instance()->CompletedQuestsBitmask.SequenceEqual(this.cachedCompletedQuests)) + { + this.cachedCompletedQuests = QuestManager.Instance()->CompletedQuestsBitmask.ToArray(); + changed |= true; + } + return changed; } } From c4dd75bdda5f5d0c375801cd54161b96ee9b57c5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 2 Oct 2025 19:36:52 +0200 Subject: [PATCH 123/477] Update RecipeData when levels changed --- Dalamud/Game/UnlockState/RecipeData.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 4c89c2f3e..9a2f95b53 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -53,6 +53,7 @@ internal unsafe class RecipeData : IInternalDisposableService this.clientState.Login += this.Update; this.clientState.Logout += this.OnLogout; + this.clientState.LevelChanged += this.OnlevelChanged; this.gameGui.UnlocksUpdate += this.Update; } @@ -61,6 +62,7 @@ internal unsafe class RecipeData : IInternalDisposableService { this.clientState.Login -= this.Update; this.clientState.Logout -= this.OnLogout; + this.clientState.LevelChanged -= this.OnlevelChanged; this.gameGui.UnlocksUpdate -= this.Update; } @@ -110,9 +112,18 @@ internal unsafe class RecipeData : IInternalDisposableService this.cachedCompletedQuests = null; } + private void OnlevelChanged(uint classJobId, uint level) + { + if (this.dataManager.GetExcelSheet().TryGetRow(classJobId, out var classJobRow) && + classJobRow.ClassJobCategory.RowId == 33) // Crafter + { + this.Update(); + } + } + private void Update() { - // Client::Game::UI::RecipeNote.InitializeStructs + // based on Client::Game::UI::RecipeNote.InitializeStructs if (!this.NeedsUpdate()) return; From 3746c47a84331538e5ec3cf9cbb130bf30627987 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 2 Oct 2025 19:39:49 +0200 Subject: [PATCH 124/477] Ignore RecipeData updates when not logged in Just to be safe... --- Dalamud/Game/UnlockState/RecipeData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 9a2f95b53..48409248a 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -125,7 +125,7 @@ internal unsafe class RecipeData : IInternalDisposableService { // based on Client::Game::UI::RecipeNote.InitializeStructs - if (!this.NeedsUpdate()) + if (!this.clientState.IsLoggedIn || !this.NeedsUpdate()) return; Array.Clear(this.unlockedNoteBookDivisionsCount, 0, this.unlockedNoteBookDivisionsCount.Length); From 986dfa04d0145a5596e8284fce461041c383f723 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 7 Oct 2025 17:42:16 +0200 Subject: [PATCH 125/477] Mark IUnlockState as experimental --- Dalamud/Game/UnlockState/UnlockState.cs | 2 ++ Dalamud/Plugin/Services/IUnlockState.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index ff1effdd1..b60e9ccdf 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -20,6 +20,8 @@ using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; namespace Dalamud.Game.UnlockState; +#pragma warning disable UnlockState + /// /// This class provides unlock state of various content in the game. /// diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index d3620ffe2..22ef94eb2 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + using Lumina.Excel; using Lumina.Excel.Sheets; @@ -8,6 +10,7 @@ namespace Dalamud.Plugin.Services; /// /// Interface for determining unlock state of various content in the game. /// +[Experimental("UnlockState")] public interface IUnlockState { /// From 878080d66076c3b2b96c86668f8409548a617584 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 16 Oct 2025 00:56:45 +0200 Subject: [PATCH 126/477] Fix IsChocoboTaxiStandUnlocked call --- Dalamud/Game/UnlockState/UnlockState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index b60e9ccdf..ae8fc4f86 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -174,7 +174,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (!this.IsLoaded) return false; - return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId - 0x120000); + return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId); } /// From 68c02caf37495a14bcd78fea6d58245274746772 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 01:10:35 +0200 Subject: [PATCH 127/477] Fix obsolete warnings --- Dalamud/Game/UnlockState/RecipeData.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 48409248a..593b7fd38 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -212,7 +212,7 @@ internal unsafe class RecipeData : IInternalDisposableService _ => default, }; - if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.TryCheckBitInSpan(bitIndex, out var result) && result) + if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get(bitIndex)) { secretRecipeBookUnlocked = true; break; @@ -254,15 +254,15 @@ internal unsafe class RecipeData : IInternalDisposableService } } - if (this.cachedUnlockedSecretRecipeBooks == null || !PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.SequenceEqual(this.cachedUnlockedSecretRecipeBooks)) + if (this.cachedUnlockedSecretRecipeBooks == null || !PlayerState.Instance()->UnlockedSecretRecipeBooks.SequenceEqual(this.cachedUnlockedSecretRecipeBooks)) { - this.cachedUnlockedSecretRecipeBooks = PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.ToArray(); + this.cachedUnlockedSecretRecipeBooks = PlayerState.Instance()->UnlockedSecretRecipeBooks.ToArray(); changed |= true; } - if (this.cachedUnlockLinks == null || !UIState.Instance()->UnlockLinkBitmask.SequenceEqual(this.cachedUnlockLinks)) + if (this.cachedUnlockLinks == null || !UIState.Instance()->UnlockLinks.SequenceEqual(this.cachedUnlockLinks)) { - this.cachedUnlockLinks = UIState.Instance()->UnlockLinkBitmask.ToArray(); + this.cachedUnlockLinks = UIState.Instance()->UnlockLinks.ToArray(); changed |= true; } From 6e8efabc3b88f396c66e66556244255afa8e5436 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 02:32:43 +0200 Subject: [PATCH 128/477] Add support for Occult Record items --- Dalamud/Game/ItemActionType.cs | 76 ----------------- Dalamud/Game/UnlockState/ItemActionType.cs | 97 ++++++++++++++++++++++ Dalamud/Game/UnlockState/UnlockState.cs | 26 +++--- 3 files changed, 111 insertions(+), 88 deletions(-) delete mode 100644 Dalamud/Game/ItemActionType.cs create mode 100644 Dalamud/Game/UnlockState/ItemActionType.cs diff --git a/Dalamud/Game/ItemActionType.cs b/Dalamud/Game/ItemActionType.cs deleted file mode 100644 index 3f2ac5f17..000000000 --- a/Dalamud/Game/ItemActionType.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Lumina.Excel.Sheets; - -namespace Dalamud.Game; - -/// -/// Enum for . -/// -public enum ItemActionType : ushort -{ - /// - /// Used to unlock a companion (minion). - /// - Companion = 853, - - /// - /// Used to unlock a chocobo companion barding. - /// - BuddyEquip = 1013, - - /// - /// Used to unlock a mount. - /// - Mount = 1322, - - /// - /// Used to unlock recipes from a crafting recipe book. - /// - SecretRecipeBook = 2136, - - /// - /// Used to unlock various types of content (e.g. Riding Maps, Blue Mage Totems, Emotes, Hairstyles). - /// - UnlockLink = 2633, - - /// - /// Used to unlock a Triple Triad Card. - /// - TripleTriadCard = 3357, - - /// - /// Used to unlock gathering nodes of a Folklore Tome. - /// - FolkloreTome = 4107, - - /// - /// Used to unlock an Orchestrion Roll. - /// - OrchestrionRoll = 25183, - - /// - /// Used to unlock portrait designs. - /// - FramersKit = 29459, - - /// - /// Used to unlock Bozjan Field Notes. These are server-side but are cached client-side. - /// - FieldNotes = 19743, - - /// - /// Used to unlock an Ornament (fashion accessory). - /// - Ornament = 20086, - - /// - /// Used to unlock glasses. - /// - Glasses = 37312, - - /// - /// Used for Company Seal Vouchers, which convert the item into Company Seals when used.
- /// Can be used only if in a Grand Company.
- /// IsUnlocked always returns false. - ///
- CompanySealVouchers = 41120, -} diff --git a/Dalamud/Game/UnlockState/ItemActionType.cs b/Dalamud/Game/UnlockState/ItemActionType.cs new file mode 100644 index 000000000..741dcd31b --- /dev/null +++ b/Dalamud/Game/UnlockState/ItemActionType.cs @@ -0,0 +1,97 @@ +using Lumina.Excel.Sheets; + +namespace Dalamud.Game.UnlockState; + +// TODO: Switch to FFXIVClientStructs.FFXIV.Client.Enums.ItemActionType. + +/// +/// Enum for . +/// +internal enum ItemActionType : ushort +{ + /// + /// No item action. + /// + None = 0, + + /// + /// Unlocks a companion (minion). + /// + Companion = 853, + + /// + /// Unlocks a chocobo companion barding. + /// + BuddyEquip = 1013, + + /// + /// Unlocks a mount. + /// + Mount = 1322, + + /// + /// Unlocks recipes from a crafting recipe book. + /// + SecretRecipeBook = 2136, + + /// + /// Unlocks various types of content (e.g. Riding Maps, Blue Mage Totems, Emotes, Hairstyles). + /// + UnlockLink = 2633, + + /// + /// Unlocks a Triple Triad Card. + /// + TripleTriadCard = 3357, + + /// + /// Unlocks gathering nodes of a Folklore Tome. + /// + FolkloreTome = 4107, + + /// + /// Unlocks an Orchestrion Roll. + /// + OrchestrionRoll = 25183, + + /// + /// Unlocks portrait designs. + /// + FramersKit = 29459, + + /// + /// Unlocks Bozjan Field Notes. + /// + /// These are server-side but are cached client-side. + FieldNotes = 19743, + + /// + /// Unlocks an Ornament (fashion accessory). + /// + Ornament = 20086, + + /// + /// Unlocks Glasses. + /// + Glasses = 37312, + + /// + /// Company Seal Vouchers, which convert the item into Company Seals when used. + /// + CompanySealVouchers = 41120, + + /// + /// Unlocks Occult Records in Occult Crescent. + /// + OccultRecords = 43141, + + /// + /// Unlocks Phantom Jobs in Occult Crescent. + /// + SoulShards = 43142, + + /// + /// Star Contributor Certificate, which grants the Star Contributor status in Cosmic Exploration. + /// + StarContributorCertificate = 45189, +} diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index ae8fc4f86..e2528c7e7 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -263,6 +263,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]); case ItemActionType.UnlockLink: + case ItemActionType.OccultRecords: return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]); case ItemActionType.TripleTriadCard when row.AdditionalData.Is(): @@ -387,18 +388,19 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (row.ItemAction.RowId == 0) return false; - return (ItemActionType)row.ItemAction.Value.Type is - ItemActionType.Companion or - ItemActionType.BuddyEquip or - ItemActionType.Mount or - ItemActionType.SecretRecipeBook or - ItemActionType.UnlockLink or - ItemActionType.TripleTriadCard or - ItemActionType.FolkloreTome or - ItemActionType.OrchestrionRoll or - ItemActionType.FramersKit or - ItemActionType.Ornament or - ItemActionType.Glasses; + return (ItemActionType)row.ItemAction.Value.Type + is ItemActionType.Companion + or ItemActionType.BuddyEquip + or ItemActionType.Mount + or ItemActionType.SecretRecipeBook + or ItemActionType.UnlockLink + or ItemActionType.TripleTriadCard + or ItemActionType.FolkloreTome + or ItemActionType.OrchestrionRoll + or ItemActionType.FramersKit + or ItemActionType.Ornament + or ItemActionType.Glasses + or ItemActionType.OccultRecords; } /// From 193d321103f78f23e79c6c3ddf5d864830d7fd30 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 02:33:06 +0200 Subject: [PATCH 129/477] Add support for Soul Shard items --- Dalamud/Game/UnlockState/UnlockState.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index e2528c7e7..14ea9e2c9 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -8,6 +8,7 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Component.Exd; @@ -284,6 +285,10 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState case ItemActionType.Glasses: return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId); + case ItemActionType.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: + var supportJobId = (byte)row.ItemAction.Value.Data[0]; + return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0; + case ItemActionType.CompanySealVouchers: return false; } @@ -400,7 +405,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState or ItemActionType.FramersKit or ItemActionType.Ornament or ItemActionType.Glasses - or ItemActionType.OccultRecords; + or ItemActionType.OccultRecords + or ItemActionType.SoulShards; } /// @@ -625,6 +631,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - Achievements // - Titles // - Bozjan Field Notes + // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 } private void UpdateUnlocksForSheet(bool fireEvent = true) where T : struct, IExcelRow From 880add5ab374afa7fa7203c9002346fd8265715d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 02:50:38 +0200 Subject: [PATCH 130/477] Add support for MKDLore rows --- Dalamud/Game/UnlockState/UnlockState.cs | 13 +++++++++++++ Dalamud/Plugin/Services/IUnlockState.cs | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 14ea9e2c9..a016b91cb 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -309,6 +309,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return this.IsUnlockLinkUnlocked(row.UnlockLink); } + /// + public bool IsMKDLoreUnlocked(MKDLore row) + { + return this.IsUnlockLinkUnlocked(row.Unknown2); + } + /// public bool IsMountUnlocked(Mount row) { @@ -493,6 +499,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (rowRef.TryGetValue(out var mjiLandmarkRow)) return this.IsMJILandmarkUnlocked(mjiLandmarkRow); + if (rowRef.TryGetValue(out var mkdLoreRow)) + return this.IsMKDLoreUnlocked(mkdLoreRow); + if (rowRef.TryGetValue(out var mcGuffinRow)) return this.IsMcGuffinUnlocked(mcGuffinRow); @@ -599,6 +608,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); @@ -772,6 +782,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// public bool IsMJILandmarkUnlocked(MJILandmark row) => this.unlockStateService.IsMJILandmarkUnlocked(row); + /// + public bool IsMKDLoreUnlocked(MKDLore row) => this.unlockStateService.IsMKDLoreUnlocked(row); + /// public bool IsMountUnlocked(Mount row) => this.unlockStateService.IsMountUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 22ef94eb2..79f68416e 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -206,6 +206,13 @@ public interface IUnlockState /// if unlocked; otherwise, . bool IsMJILandmarkUnlocked(MJILandmark row); + /// + /// Determines whether the specified MKDLore (Occult Record) is unlocked. + /// + /// The MKDLore row to check. + /// if unlocked; otherwise, . + bool IsMKDLoreUnlocked(MKDLore row); + /// /// Determines whether the specified Mount is unlocked. /// From a06c0e3ed2497823a168c322b82ed5b7e40e2cd2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 03:51:38 +0200 Subject: [PATCH 131/477] Add support for EmjVoiceNpc rows --- Dalamud/Game/UnlockState/UnlockState.cs | 10 ++++++++++ Dalamud/Plugin/Services/IUnlockState.cs | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index a016b91cb..0d4a8859e 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -205,6 +205,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return this.IsUnlockLinkUnlocked(row.UnlockLink); } + /// + public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) + { + return this.IsUnlockLinkUnlocked(row.Unknown26); + } + /// public bool IsGeneralActionUnlocked(GeneralAction row) { @@ -601,6 +607,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); @@ -758,6 +765,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// public bool IsEmoteUnlocked(Emote row) => this.unlockStateService.IsEmoteUnlocked(row); + /// + public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) => this.unlockStateService.IsEmjVoiceNpcUnlocked(row); + /// public bool IsGeneralActionUnlocked(GeneralAction row) => this.unlockStateService.IsGeneralActionUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 79f68416e..50167812d 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -150,6 +150,13 @@ public interface IUnlockState /// if unlocked; otherwise, . bool IsEmoteUnlocked(Emote row); + /// + /// Determines whether the specified EmjVoiceNpc (Doman Mahjong Characters) is unlocked. + /// + /// The EmjVoiceNpc row to check. + /// if unlocked; otherwise, . + bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row); + /// /// Determines whether the specified GeneralAction is unlocked. /// From 69caffeb97e31d5ec5c58e75ef34ca4de5f733f5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 03:52:31 +0200 Subject: [PATCH 132/477] Add support for EmjCostume rows --- Dalamud/Game/UnlockState/UnlockState.cs | 15 +++++++++++++++ Dalamud/Plugin/Services/IUnlockState.cs | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 0d4a8859e..f86b89efb 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -8,6 +8,7 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Component.Exd; @@ -211,6 +212,14 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return this.IsUnlockLinkUnlocked(row.Unknown26); } + /// + public bool IsEmjCostumeUnlocked(EmjCostume row) + { + return this.dataManager.GetExcelSheet().TryGetRow(row.RowId, out var emjVoiceNpcRow) + && this.IsEmjVoiceNpcUnlocked(emjVoiceNpcRow) + && QuestManager.IsQuestComplete(row.Unknown1); + } + /// public bool IsGeneralActionUnlocked(GeneralAction row) { @@ -644,6 +653,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - FramersKit (is that just an Item?) // - ... more? + // Subrow sheets, which are incompatible with the current Unlock event, since RowRef doesn't carry the SubrowId: + // - EmjCostume + // Probably not happening, because it requires fetching data from server: // - Achievements // - Titles @@ -768,6 +780,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) => this.unlockStateService.IsEmjVoiceNpcUnlocked(row); + /// + public bool IsEmjCostumeUnlocked(EmjCostume row) => this.unlockStateService.IsEmjCostumeUnlocked(row); + /// public bool IsGeneralActionUnlocked(GeneralAction row) => this.unlockStateService.IsGeneralActionUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 50167812d..00f2df190 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -157,6 +157,13 @@ public interface IUnlockState /// if unlocked; otherwise, . bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row); + /// + /// Determines whether the specified EmjCostume (Doman Mahjong Character Costume) is unlocked. + /// + /// The EmjCostume row to check. + /// if unlocked; otherwise, . + bool IsEmjCostumeUnlocked(EmjCostume row); + /// /// Determines whether the specified GeneralAction is unlocked. /// From 700aaa4a5d647a74f657088f0d38a85694a3a28f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 19 Oct 2025 17:20:30 +0200 Subject: [PATCH 133/477] Fix Unlock event not firing --- Dalamud/Game/UnlockState/UnlockState.cs | 111 ++++++++++-------------- 1 file changed, 48 insertions(+), 63 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index f86b89efb..846be8294 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -577,65 +577,53 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(unlockLink); } - private void UpdateUnlocks() - { - try - { - this.UpdateUnlocks(false); - } - catch (Exception ex) - { - Log.Error(ex, "Error during initial unlock check"); - } - } - private void OnLogout(int type, int code) { this.cachedUnlockedRowIds.Clear(); } - private void UpdateUnlocks(bool fireEvent) + private void UpdateUnlocks() { if (!this.IsLoaded) return; - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); // Not implemented: // - DescriptionPage: quite complex @@ -663,7 +651,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 } - private void UpdateUnlocksForSheet(bool fireEvent = true) where T : struct, IExcelRow + private void UpdateUnlocksForSheet() where T : struct, IExcelRow { var unlockedRowIds = this.cachedUnlockedRowIds.GetOrAdd(typeof(T), _ => []); @@ -679,20 +667,17 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState unlockedRowIds.Add(row.RowId); - if (fireEvent) - { - Log.Verbose("Unlock detected: {row}", $"{typeof(T).Name}#{row.RowId}"); + Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}"); - foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + { + try { - try - { - action((RowRef)rowRef); - } - catch (Exception ex) - { - Log.Error(ex, "Exception during raise of {handler}", action.Method); - } + action((RowRef)rowRef); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); } } } From af8b61f08a4759512444f0d11c0e89e2b973c699 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 8 Nov 2025 11:47:31 +0100 Subject: [PATCH 134/477] Update to use AgentUpdate event --- Dalamud/Game/UnlockState/RecipeData.cs | 10 ++++++++-- Dalamud/Game/UnlockState/UnlockState.cs | 21 ++++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 593b7fd38..b96cab951 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -54,7 +54,7 @@ internal unsafe class RecipeData : IInternalDisposableService this.clientState.Login += this.Update; this.clientState.Logout += this.OnLogout; this.clientState.LevelChanged += this.OnlevelChanged; - this.gameGui.UnlocksUpdate += this.Update; + this.gameGui.AgentUpdate += this.OnAgentUpdate; } /// @@ -63,7 +63,7 @@ internal unsafe class RecipeData : IInternalDisposableService this.clientState.Login -= this.Update; this.clientState.Logout -= this.OnLogout; this.clientState.LevelChanged -= this.OnlevelChanged; - this.gameGui.UnlocksUpdate -= this.Update; + this.gameGui.AgentUpdate -= this.OnAgentUpdate; } /// @@ -121,6 +121,12 @@ internal unsafe class RecipeData : IInternalDisposableService } } + private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag) + { + if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate)) + this.Update(); + } + private void Update() { // based on Client::Game::UI::RecipeNote.InitializeStructs diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 846be8294..a4b9381cc 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -49,9 +49,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState [ServiceManager.ServiceConstructor] private UnlockState() { - this.clientState.Login += this.UpdateUnlocks; + this.clientState.Login += this.OnLogin; this.clientState.Logout += this.OnLogout; - this.gameGui.UnlocksUpdate += this.UpdateUnlocks; + this.gameGui.AgentUpdate += this.OnAgentUpdate; } /// @@ -62,9 +62,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState /// void IInternalDisposableService.DisposeService() { - this.clientState.Login -= this.UpdateUnlocks; + this.clientState.Login -= this.OnLogin; this.clientState.Logout -= this.OnLogout; - this.gameGui.UnlocksUpdate -= this.UpdateUnlocks; + this.gameGui.AgentUpdate -= this.OnAgentUpdate; } /// @@ -577,12 +577,23 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(unlockLink); } + private void OnLogin() + { + this.Update(); + } + private void OnLogout(int type, int code) { this.cachedUnlockedRowIds.Clear(); } - private void UpdateUnlocks() + private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag) + { + if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate)) + this.Update(); + } + + private void Update() { if (!this.IsLoaded) return; From 5cc327c5f9abd1439e81ec2eac486a1e6070081b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 8 Nov 2025 11:49:23 +0100 Subject: [PATCH 135/477] Fix obsolete --- Dalamud/Game/UnlockState/RecipeData.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index b96cab951..c419ba4fd 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -272,9 +272,9 @@ internal unsafe class RecipeData : IInternalDisposableService changed |= true; } - if (this.cachedCompletedQuests == null || !QuestManager.Instance()->CompletedQuestsBitmask.SequenceEqual(this.cachedCompletedQuests)) + if (this.cachedCompletedQuests == null || !QuestManager.Instance()->CompletedQuests.SequenceEqual(this.cachedCompletedQuests)) { - this.cachedCompletedQuests = QuestManager.Instance()->CompletedQuestsBitmask.ToArray(); + this.cachedCompletedQuests = QuestManager.Instance()->CompletedQuests.ToArray(); changed |= true; } From 497e61f699e9ccff29a48a3a37ea0f8f2fc7de00 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 8 Nov 2025 11:58:54 +0100 Subject: [PATCH 136/477] Remove comment about removed CS enum --- Dalamud/Game/UnlockState/ItemActionType.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dalamud/Game/UnlockState/ItemActionType.cs b/Dalamud/Game/UnlockState/ItemActionType.cs index 741dcd31b..8e3d79b84 100644 --- a/Dalamud/Game/UnlockState/ItemActionType.cs +++ b/Dalamud/Game/UnlockState/ItemActionType.cs @@ -2,8 +2,6 @@ using Lumina.Excel.Sheets; namespace Dalamud.Game.UnlockState; -// TODO: Switch to FFXIVClientStructs.FFXIV.Client.Enums.ItemActionType. - /// /// Enum for . /// From 5dd121dfcc866d7838768729772259996adb6302 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 8 Nov 2025 17:02:16 +0100 Subject: [PATCH 137/477] Update Excel Schema (#2430) Co-authored-by: github-actions[bot] --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index a37961c45..5d01489c3 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit a37961c453463f0f60a96f27aa278ef139841857 +Subproject commit 5d01489c34f33a3d645f49085d7fc0065a1ac801 From af03e292bad7899c58329f178166adcab39ecf93 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 11 Nov 2025 09:09:29 +0100 Subject: [PATCH 138/477] Pass down SelfTestRegistry to SelfTestWindow --- Dalamud/Interface/Internal/DalamudInterface.cs | 6 ++++-- .../Interface/Internal/Windows/SelfTest/SelfTestWindow.cs | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 7b105b914..05ecff8d9 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -33,6 +33,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; +using Dalamud.Plugin.SelfTest.Internal; using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.System.Framework; @@ -103,7 +104,8 @@ internal class DalamudInterface : IInternalDisposableService TitleScreenMenu titleScreenMenu, GameGui gameGui, ConsoleManager consoleManager, - AddonLifecycle addonLifecycle) + AddonLifecycle addonLifecycle, + SelfTestRegistry selfTestRegistry) { this.dalamud = dalamud; this.configuration = configuration; @@ -119,7 +121,7 @@ internal class DalamudInterface : IInternalDisposableService this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; this.pluginWindow = new PluginInstallerWindow(pluginImageCache, configuration) { IsOpen = false }; this.settingsWindow = new SettingsWindow() { IsOpen = false }; - this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; + this.selfTestWindow = new SelfTestWindow(selfTestRegistry) { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; this.titleScreenMenuWindow = new TitleScreenMenuWindow( clientState, diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index b19b4cf96..ea8cd0070 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -13,7 +13,6 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.SelfTest; using Dalamud.Plugin.SelfTest.Internal; using Dalamud.Utility; -using Lumina.Excel.Sheets; namespace Dalamud.Interface.Internal.Windows.SelfTest; @@ -36,10 +35,11 @@ internal class SelfTestWindow : Window /// /// Initializes a new instance of the class. /// - public SelfTestWindow() + /// An instance of . + public SelfTestWindow(SelfTestRegistry selfTestRegistry) : base("Dalamud Self-Test", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { - this.selfTestRegistry = Service.Get(); + this.selfTestRegistry = selfTestRegistry; this.Size = new Vector2(800, 800); this.SizeCondition = ImGuiCond.FirstUseEver; From 963b3d9318d15f2855b4a02c7c0e4f4f1c3b2ef9 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:27:07 +0100 Subject: [PATCH 139/477] Update ClientStructs (#2447) 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 2b2d67e37..f6c479b3f 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 2b2d67e374b9f117d3d8038070bd80909de3941d +Subproject commit f6c479b3fa0b452b44403c8ea53d592bec415e1e From fe163fbb970d28816828febe44cb2a52904f650f Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 11 Nov 2025 08:28:14 -0800 Subject: [PATCH 140/477] fix: some minor IDE complaints --- Dalamud/Game/Internal/DalamudCompletion.cs | 4 ++-- Dalamud/Interface/Internal/Asserts/AssertHandler.cs | 1 - Dalamud/Interface/Internal/UiDebug.cs | 4 ++-- .../Internal/UiDebug2/Browsing/NodeTree.Component.cs | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Internal/DalamudCompletion.cs b/Dalamud/Game/Internal/DalamudCompletion.cs index ec5652b3c..e3564c823 100644 --- a/Dalamud/Game/Internal/DalamudCompletion.cs +++ b/Dalamud/Game/Internal/DalamudCompletion.cs @@ -114,7 +114,7 @@ internal sealed unsafe class DalamudCompletion : IInternalDisposableService this.ResetCompletionData(); this.ClearCachedCommands(); - var currentText = component->UnkText1.StringPtr.ExtractText(); + var currentText = component->EvaluatedString.StringPtr.ExtractText(); var commands = this.commandManager.Commands .Where(kv => kv.Value.ShowInHelp && (currentText.Length == 0 || kv.Key.StartsWith(currentText))) @@ -195,7 +195,7 @@ internal sealed unsafe class DalamudCompletion : IInternalDisposableService component = (AtkComponentTextInput*)componentBase; - addon = component->ContainingAddon; + addon = component->OwnerAddon; if (addon == null) addon = component->ContainingAddon2; diff --git a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs index a905ec132..276dddb57 100644 --- a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs +++ b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs @@ -110,7 +110,6 @@ internal class AssertHandler : IDisposable return new StackTrace(frames); } - private unsafe void OnImGuiAssert(void* pExpr, void* pFile, int line) { var expr = Marshal.PtrToStringAnsi(new IntPtr(pExpr)); diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 82aec5c41..1211b505d 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -414,11 +414,11 @@ internal unsafe class UiDebug var textInputComponent = (AtkComponentTextInput*)compNode->Component; ImGui.Text("InputBase Text1: "u8); ImGui.SameLine(); - Service.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText1); + Service.Get().Draw(textInputComponent->AtkComponentInputBase.EvaluatedString); ImGui.Text("InputBase Text2: "u8); ImGui.SameLine(); - Service.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText2); + Service.Get().Draw(textInputComponent->AtkComponentInputBase.RawString); ImGui.Text("Text1: "u8); ImGui.SameLine(); diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs index b3e7d1b21..a35195498 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs @@ -90,9 +90,9 @@ internal unsafe class ComponentNodeTree : ResNodeTree case TextInput: var textInputComponent = (AtkComponentTextInput*)this.Component; ImGui.Text( - $"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}"); + $"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.EvaluatedString.StringPtr))}"); ImGui.Text( - $"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}"); + $"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.RawString.StringPtr))}"); ImGui.Text( $"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}"); ImGui.Text( From dabe7d777be88b98f7adffac293302507915c9bf Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 11 Nov 2025 19:27:46 +0100 Subject: [PATCH 141/477] build: 13.0.0.8 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 890202967..1166c24c0 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.7 + 13.0.0.8 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 65237f84a22a561d719f57ffb1cc8013f70f20ef Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 10 Nov 2025 04:41:01 +0100 Subject: [PATCH 142/477] Add functions to get a plugin by assembly This is intended for advanced IPC scenarios, for example, accepting a delegate or an object and identifying which plugin it originates from, in order to display integration information to the user, and/or to release references when the originating plugin is unloaded/reloaded if it forgot to clean after itself. --- Dalamud/Plugin/DalamudPluginInterface.cs | 25 ++++++++++++++++++++ Dalamud/Plugin/IDalamudPluginInterface.cs | 16 +++++++++++++ Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 9 +++++++ 3 files changed, 50 insertions(+) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 39a4e7e4b..957e8080a 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.Loader; using System.Threading.Tasks; using Dalamud.Configuration; @@ -269,6 +270,30 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa return true; } + /// + /// Gets the plugin the given assembly is part of. + /// + /// The assembly to check. + /// The plugin the given assembly is part of, or null if this is a shared assembly or if this information cannot be determined. + public IExposedPlugin? GetPlugin(Assembly assembly) + => AssemblyLoadContext.GetLoadContext(assembly) switch + { + null => null, + var context => this.GetPlugin(context), + }; + + /// + /// Gets the plugin that loads in the given context. + /// + /// The context to check. + /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. + public IExposedPlugin? GetPlugin(AssemblyLoadContext context) + => Service.Get().InstalledPlugins.FirstOrDefault(p => p.LoadsIn(context)) switch + { + null => null, + var p => new ExposedPlugin(p), + }; + #region IPC /// diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index b8ab55450..e1dd34f87 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reflection; +using System.Runtime.Loader; using System.Threading.Tasks; using Dalamud.Configuration; @@ -180,6 +182,20 @@ public interface IDalamudPluginInterface /// Returns false if the DalamudInterface was null. bool OpenDeveloperMenu(); + /// + /// Gets the plugin the given assembly is part of. + /// + /// The assembly to check. + /// The plugin the given assembly is part of, or null if this is a shared assembly or if this information cannot be determined. + IExposedPlugin? GetPlugin(Assembly assembly); + + /// + /// Gets the plugin that loads in the given context. + /// + /// The context to check. + /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. + IExposedPlugin? GetPlugin(AssemblyLoadContext context); + /// T GetOrCreateData(string tag, Func dataGenerator) where T : class; diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index da8ec8ff9..0197683ef 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; +using System.Runtime.Loader; using System.Threading; using System.Threading.Tasks; @@ -553,6 +554,14 @@ internal class LocalPlugin : IAsyncDisposable }); } + /// + /// Checks whether this plugin loads in the given load context. + /// + /// The load context to check. + /// Whether this plugin loads in the given load context. + public bool LoadsIn(AssemblyLoadContext context) + => this.loader?.LoadContext == context; + /// /// Save this plugin manifest. /// From f6cd6d31ff26e6d7823f7e37bdde7a36eb4fb3dd Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 11 Nov 2025 20:29:06 +0100 Subject: [PATCH 143/477] Adjust branch switcher to XL 7, pass beta kind and key as arguments --- .../Internal/Windows/BranchSwitcherWindow.cs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index 5caf8b5be..f7250e528 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -83,25 +83,12 @@ public class BranchSwitcherWindow : Window ImGuiHelpers.ScaledDummy(5); - void Pick() + if (ImGui.Button("Pick & Restart"u8)) { var config = Service.Get(); config.DalamudBetaKind = pickedBranch.Key; config.DalamudBetaKey = pickedBranch.Value.Key; config.QueueSave(); - } - - if (ImGui.Button("Pick"u8)) - { - Pick(); - this.IsOpen = false; - } - - ImGui.SameLine(); - - if (ImGui.Button("Pick & Restart"u8)) - { - Pick(); // If we exit immediately, we need to write out the new config now Service.Get().ForceSave(); @@ -111,7 +98,16 @@ public class BranchSwitcherWindow : Window if (File.Exists(xlPath)) { - Process.Start(xlPath); + var ps = new ProcessStartInfo + { + FileName = xlPath, + UseShellExecute = false, + }; + + ps.ArgumentList.Add($"--dalamud-beta-kind={config.DalamudBetaKind}"); + ps.ArgumentList.Add($"--dalamud-beta-key={config.DalamudBetaKey}"); + + Process.Start(ps); Environment.Exit(0); } } From 4cfe561c1c56f419eb23e7752315422909379397 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 11 Nov 2025 21:28:30 +0100 Subject: [PATCH 144/477] Add a property to get the bounds of a DTR entry --- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index 6fdc504ca..f5b7011fe 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -1,4 +1,6 @@ -using Dalamud.Configuration.Internal; +using System.Numerics; + +using Dalamud.Configuration.Internal; using Dalamud.Game.Addon.Events.EventDataTypes; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Internal.Types; @@ -48,6 +50,11 @@ public interface IReadOnlyDtrBarEntry /// Gets an action to be invoked when the user clicks on the dtr entry. /// public Action? OnClick { get; } + + /// + /// Gets the axis-aligned bounding box of this entry, in screen coordinates. + /// + public (Vector2 Min, Vector2 Max) ScreenBounds { get; } } /// @@ -146,6 +153,17 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry [Api13ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; + /// + public (Vector2 Min, Vector2 Max) ScreenBounds + => this.TextNode switch + { + null => default, + var node => node->IsVisible() + ? (new(node->ScreenX, node->ScreenY), + new(node->ScreenX + node->GetWidth(), node->ScreenY + node->GetHeight())) + : default, + }; + /// /// Gets or sets the internal text node of this entry. /// From bf0bd64faff9c66f2d0e5bb7094658d3aafaa9a5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 11 Nov 2025 23:37:10 +0100 Subject: [PATCH 145/477] Fix IsAutoUpdateComplete throwing when unloaded --- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 39a4e7e4b..53115d179 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -103,7 +103,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// /// Gets a value indicating whether auto-updates have already completed this session. /// - public bool IsAutoUpdateComplete => Service.Get().IsAutoUpdateComplete; + public bool IsAutoUpdateComplete => Service.GetNullable()?.IsAutoUpdateComplete ?? false; /// /// Gets the repository from which this plugin was installed. From e1fde804ecf50f57b91e27a8a8e4863eb24f580a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 12 Nov 2025 00:21:47 +0100 Subject: [PATCH 146/477] Add warning if RemoveNode can't find the node --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 920978b00..dabe292bd 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -546,12 +546,16 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar { var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - + if (this.eventHandles.TryGetValue(node->AtkResNode.NodeId, out var eventHandles)) { eventHandles.ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); eventHandles.Clear(); } + else + { + Log.Warning("Could not find AtkResNode with NodeId {nodeId} in eventHandles", node->AtkResNode.NodeId); + } var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; From f635c149a236389934bd7a6850843d3612589772 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 12 Nov 2025 00:23:08 +0100 Subject: [PATCH 147/477] Set TextNode to null after destroying it --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index dabe292bd..b5c671a6f 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -257,7 +257,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar /// The resources to remove. internal void RemoveEntry(DtrBarEntry toRemove) { - this.RemoveNode(toRemove.TextNode); + this.RemoveNode(toRemove); if (toRemove.Storage != null) { @@ -542,9 +542,10 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar return true; } - private void RemoveNode(AtkTextNode* node) + private void RemoveNode(DtrBarEntry data) { var dtr = this.GetDtr(); + var node = data.TextNode; if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; if (this.eventHandles.TryGetValue(node->AtkResNode.NodeId, out var eventHandles)) @@ -565,6 +566,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar if (tmpPrevNode != null) tmpPrevNode->NextSiblingNode = tmpNextNode; node->AtkResNode.Destroy(true); + data.TextNode = null; dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1); Log.Debug("Set last sibling of DTR and updated child count"); From 45bd30fccac0702981bdf7509353616c3f30cc4e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 12 Nov 2025 00:27:51 +0100 Subject: [PATCH 148/477] Use inherited fields --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 48 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index b5c671a6f..2235c5ade 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -378,12 +378,12 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar var isHide = !data.Shown || data.UserHidden; var node = data.TextNode; - var nodeHidden = !node->AtkResNode.IsVisible(); + var nodeHidden = !node->IsVisible(); if (!isHide) { if (nodeHidden) - node->AtkResNode.ToggleVisibility(true); + node->ToggleVisibility(true); if (data is { Added: true, Text: not null, TextNode: not null } && (data.Dirty || nodeHidden)) { @@ -397,27 +397,27 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar ushort w = 0, h = 0; node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); - node->AtkResNode.SetWidth(w); + node->SetWidth(w); } - var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing; + var elementWidth = data.TextNode->Width + this.configuration.DtrSpacing; if (this.configuration.DtrSwapDirection) { - data.TextNode->AtkResNode.SetPositionFloat(runningXPos + this.configuration.DtrSpacing, 2); + data.TextNode->SetPositionFloat(runningXPos + this.configuration.DtrSpacing, 2); runningXPos += elementWidth; } else { runningXPos -= elementWidth; - data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); + data.TextNode->SetPositionFloat(runningXPos, 2); } } else if (!nodeHidden) { // If we want the node hidden, shift it up, to prevent collision conflicts - node->AtkResNode.SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX); - node->AtkResNode.ToggleVisibility(false); + node->SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX); + node->ToggleVisibility(false); } data.Dirty = false; @@ -516,8 +516,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar var node = data.TextNode = this.MakeNode(++this.runningNodeIds); - this.eventHandles.TryAdd(node->AtkResNode.NodeId, new List()); - this.eventHandles[node->AtkResNode.NodeId].AddRange(new List + this.eventHandles.TryAdd(node->NodeId, new List()); + this.eventHandles[node->NodeId].AddRange(new List { this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler), this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler), @@ -528,8 +528,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; Log.Debug($"Found last sibling: {(ulong)lastChild:X}"); lastChild->PrevSiblingNode = (AtkResNode*)node; - node->AtkResNode.ParentNode = lastChild->ParentNode; - node->AtkResNode.NextSiblingNode = lastChild; + node->ParentNode = lastChild->ParentNode; + node->NextSiblingNode = lastChild; dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount + 1); Log.Debug("Set last sibling of DTR and updated child count"); @@ -548,24 +548,24 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar var node = data.TextNode; if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - if (this.eventHandles.TryGetValue(node->AtkResNode.NodeId, out var eventHandles)) + if (this.eventHandles.TryGetValue(node->NodeId, out var eventHandles)) { eventHandles.ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); eventHandles.Clear(); } else { - Log.Warning("Could not find AtkResNode with NodeId {nodeId} in eventHandles", node->AtkResNode.NodeId); + Log.Warning("Could not find AtkResNode with NodeId {nodeId} in eventHandles", node->NodeId); } - var tmpPrevNode = node->AtkResNode.PrevSiblingNode; - var tmpNextNode = node->AtkResNode.NextSiblingNode; + var tmpPrevNode = node->PrevSiblingNode; + var tmpNextNode = node->NextSiblingNode; // if (tmpNextNode != null) tmpNextNode->PrevSiblingNode = tmpPrevNode; if (tmpPrevNode != null) tmpPrevNode->NextSiblingNode = tmpNextNode; - node->AtkResNode.Destroy(true); + node->Destroy(true); data.TextNode = null; dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1); @@ -584,13 +584,13 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar return null; } - newTextNode->AtkResNode.NodeId = nodeId; - newTextNode->AtkResNode.Type = NodeType.Text; - newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents; - newTextNode->AtkResNode.DrawFlags = 12; - newTextNode->AtkResNode.SetWidth(22); - newTextNode->AtkResNode.SetHeight(22); - newTextNode->AtkResNode.SetPositionFloat(-200, 2); + newTextNode->NodeId = nodeId; + newTextNode->Type = NodeType.Text; + newTextNode->NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents; + newTextNode->DrawFlags = 12; + newTextNode->SetWidth(22); + newTextNode->SetHeight(22); + newTextNode->SetPositionFloat(-200, 2); newTextNode->LineSpacing = 12; newTextNode->AlignmentFontType = 5; From 4e87b4b0076460195f3be5e2fe76630e41ebec2e Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 20:15:12 +0100 Subject: [PATCH 149/477] Retarget to .NET 10 --- Directory.Build.props | 2 +- global.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4ed87c809..5f6da3d94 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ - net9.0-windows + net10.0-windows x64 x64 13.0 diff --git a/global.json b/global.json index ab1a4a2ec..93dd0dd1f 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.0", + "version": "10.0.0", "rollForward": "latestMinor", "allowPrerelease": true } -} +} \ No newline at end of file From 7d76d275559bd82ef0b53a9709a8d1530f007630 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 20:31:28 +0100 Subject: [PATCH 150/477] Upgrade packages --- Directory.Packages.props | 120 ++++++++++++++++++--------------------- build/build.csproj | 2 +- 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a1cef517e..91875e63e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,65 +1,57 @@ - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/build.csproj b/build/build.csproj index b4aaa959d..32907677f 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -12,6 +12,6 @@ - + From e0eff2fe74a91a4d234c3b916da7d61760cb9c9f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:02:07 +0100 Subject: [PATCH 151/477] Use standard apphost for Dalamud.Injector --- .../Dalamud.Injector.Boot.vcxproj | 111 ------------------ .../Dalamud.Injector.Boot.vcxproj.filters | 67 ----------- Dalamud.Injector.Boot/main.cpp | 48 -------- Dalamud.Injector.Boot/pch.h | 1 - Dalamud.Injector.Boot/resources.rc | 1 - Dalamud.Injector/Dalamud.Injector.csproj | 3 +- .../{EntryPoint.cs => Program.cs} | 26 +--- .../dalamud.ico | Bin Dalamud.sln | 12 +- 9 files changed, 9 insertions(+), 260 deletions(-) delete mode 100644 Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj delete mode 100644 Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters delete mode 100644 Dalamud.Injector.Boot/main.cpp delete mode 100644 Dalamud.Injector.Boot/pch.h delete mode 100644 Dalamud.Injector.Boot/resources.rc rename Dalamud.Injector/{EntryPoint.cs => Program.cs} (98%) rename {Dalamud.Injector.Boot => Dalamud.Injector}/dalamud.ico (100%) diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj deleted file mode 100644 index 7f8de3843..000000000 --- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj +++ /dev/null @@ -1,111 +0,0 @@ - - - - {8874326B-E755-4D13-90B4-59AB263A3E6B} - Dalamud_Injector_Boot - Debug - x64 - - - - Debug - x64 - - - Release - x64 - - - - 16.0 - Win32Proj - 10.0 - Dalamud.Injector - - - - Application - true - v143 - false - Unicode - ..\bin\$(Configuration)\ - obj\$(Configuration)\ - - - - - Level3 - true - true - stdcpp23 - pch.h - ProgramDatabase - CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - - - Console - true - false - ..\lib\CoreCLR;%(AdditionalLibraryDirectories) - $(OutDir)$(TargetName).Boot.pdb - - - - - true - false - MultiThreadedDebugDLL - _DEBUG;%(PreprocessorDefinitions) - - - false - false - - - - - true - true - MultiThreadedDLL - NDEBUG;%(PreprocessorDefinitions) - - - true - true - - - - - - nethost.dll - PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters deleted file mode 100644 index 8f4372d89..000000000 --- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters +++ /dev/null @@ -1,67 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd - - - {4faac519-3a73-4b2b-96e7-fb597f02c0be} - ico;rc - - - - - Resource Files - - - - - Resource Files - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - \ No newline at end of file diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp deleted file mode 100644 index df4120009..000000000 --- a/Dalamud.Injector.Boot/main.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#define WIN32_LEAN_AND_MEAN - -#include -#include -#include -#include "..\Dalamud.Boot\logging.h" -#include "..\lib\CoreCLR\CoreCLR.h" -#include "..\lib\CoreCLR\boot.h" - -int wmain(int argc, wchar_t** argv) -{ - // Take care: don't redirect stderr/out here, we need to write our pid to stdout for XL to read - //logging::start_file_logging("dalamud.injector.boot.log", false); - logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors"); - logging::I("Built at : " __DATE__ "@" __TIME__); - - wchar_t _module_path[MAX_PATH]; - GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2); - std::filesystem::path fs_module_path(_module_path); - - std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str()); - std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str()); - - // =========================================================================== // - - void* entrypoint_vfn; - const auto result = InitializeClrAndGetEntryPoint( - GetModuleHandleW(nullptr), - false, - runtimeconfig_path, - module_path, - L"Dalamud.Injector.EntryPoint, Dalamud.Injector", - L"Main", - L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector", - &entrypoint_vfn); - - if (FAILED(result)) - return result; - - typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**); - custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn); - - logging::I("Running Dalamud Injector..."); - const auto ret = entrypoint_fn(argc, argv); - logging::I("Done!"); - - return ret; -} diff --git a/Dalamud.Injector.Boot/pch.h b/Dalamud.Injector.Boot/pch.h deleted file mode 100644 index 6f70f09be..000000000 --- a/Dalamud.Injector.Boot/pch.h +++ /dev/null @@ -1 +0,0 @@ -#pragma once diff --git a/Dalamud.Injector.Boot/resources.rc b/Dalamud.Injector.Boot/resources.rc deleted file mode 100644 index 8369e82a1..000000000 --- a/Dalamud.Injector.Boot/resources.rc +++ /dev/null @@ -1 +0,0 @@ -MAINICON ICON "dalamud.ico" diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index 4a55174a1..a0b4f6451 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -13,12 +13,13 @@ - Library + Exe ..\bin\$(Configuration)\ false false true false + dalamud.ico diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/Program.cs similarity index 98% rename from Dalamud.Injector/EntryPoint.cs rename to Dalamud.Injector/Program.cs index b876aa6ed..e224791e6 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/Program.cs @@ -25,34 +25,20 @@ namespace Dalamud.Injector /// /// Entrypoint to the program. /// - public sealed class EntryPoint + public sealed class Program { - /// - /// A delegate used during initialization of the CLR from Dalamud.Injector.Boot. - /// - /// Count of arguments. - /// char** string arguments. - /// Return value (HRESULT). - public delegate int MainDelegate(int argc, IntPtr argvPtr); - /// /// Start the Dalamud injector. /// - /// Count of arguments. - /// byte** string arguments. + /// Command line arguments. /// Return value (HRESULT). - public static int Main(int argc, IntPtr argvPtr) + public static int Main(string[] argsArray) { try { - List args = new(argc); - - unsafe - { - var argv = (IntPtr*)argvPtr; - for (var i = 0; i < argc; i++) - args.Add(Marshal.PtrToStringUni(argv[i])); - } + // API14 TODO: Refactor + var args = argsArray.ToList(); + args.Insert(0, Assembly.GetExecutingAssembly().Location); Init(args); args.Remove("-v"); // Remove "verbose" flag diff --git a/Dalamud.Injector.Boot/dalamud.ico b/Dalamud.Injector/dalamud.ico similarity index 100% rename from Dalamud.Injector.Boot/dalamud.ico rename to Dalamud.Injector/dalamud.ico diff --git a/Dalamud.sln b/Dalamud.sln index c3af00f44..ee3c75b25 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32319.34 MinimumVisualStudioVersion = 10.0.40219.1 @@ -27,8 +27,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}" @@ -49,8 +47,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}" @@ -103,10 +99,6 @@ Global {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64 @@ -188,8 +180,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490} - {8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490} {4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944} {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} {E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} From a37a13e0ba0ab5a661f71add85c0b9740dabdf1c Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:03:14 +0100 Subject: [PATCH 152/477] Use .NET 10 in CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be44afacc..299d71e95 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: uses: microsoft/setup-msbuild@v1.0.2 - uses: actions/setup-dotnet@v3 with: - dotnet-version: '9.0.200' + dotnet-version: '10.0.100' - name: Define VERSION run: | $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) From 7bc921f54328924e2b29ef481da08d481a60e4b7 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:09:21 +0100 Subject: [PATCH 153/477] No analyzers on nuke build --- build/build.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/build/build.csproj b/build/build.csproj index 32907677f..1e1416d92 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -13,5 +13,6 @@ + From 928fbba4893ae2022a5b8b637c3fa875bc4afec4 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:13:50 +0100 Subject: [PATCH 154/477] Remove Injector.Boot targets --- build/DalamudBuild.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs index d374c79f8..ba2b09a4d 100644 --- a/build/DalamudBuild.cs +++ b/build/DalamudBuild.cs @@ -42,10 +42,7 @@ public class DalamudBuild : NukeBuild AbsolutePath InjectorProjectDir => RootDirectory / "Dalamud.Injector"; AbsolutePath InjectorProjectFile => InjectorProjectDir / "Dalamud.Injector.csproj"; - - AbsolutePath InjectorBootProjectDir => RootDirectory / "Dalamud.Injector.Boot"; - AbsolutePath InjectorBootProjectFile => InjectorBootProjectDir / "Dalamud.Injector.Boot.vcxproj"; - + AbsolutePath TestProjectDir => RootDirectory / "Dalamud.Test"; AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj"; @@ -172,14 +169,6 @@ public class DalamudBuild : NukeBuild .EnableNoRestore()); }); - Target CompileInjectorBoot => _ => _ - .Executes(() => - { - MSBuildTasks.MSBuild(s => s - .SetTargetPath(InjectorBootProjectFile) - .SetConfiguration(Configuration)); - }); - Target SetCILogging => _ => _ .DependentFor(Compile) .OnlyWhenStatic(() => IsCIBuild) @@ -196,7 +185,6 @@ public class DalamudBuild : NukeBuild .DependsOn(CompileDalamudBoot) .DependsOn(CompileDalamudCrashHandler) .DependsOn(CompileInjector) - .DependsOn(CompileInjectorBoot) ; Target CI => _ => _ @@ -250,11 +238,6 @@ public class DalamudBuild : NukeBuild .SetProject(InjectorProjectFile) .SetConfiguration(Configuration)); - MSBuildTasks.MSBuild(s => s - .SetProjectFile(InjectorBootProjectFile) - .SetConfiguration(Configuration) - .SetTargets("Clean")); - FileSystemTasks.DeleteDirectory(ArtifactsDirectory); Directory.CreateDirectory(ArtifactsDirectory); }); From 6340afb6921bfd8915e22300bcf623c956ea0c1f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:39:38 +0100 Subject: [PATCH 155/477] Nuke schema, also remove analyzers from imgui testbed --- .nuke/build.schema.json | 2 -- imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 8331affcc..03211ce8f 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -87,7 +87,6 @@ "CompileDalamudCrashHandler", "CompileImGuiNatives", "CompileInjector", - "CompileInjectorBoot", "Restore", "SetCILogging", "Test" @@ -115,7 +114,6 @@ "CompileDalamudCrashHandler", "CompileImGuiNatives", "CompileInjector", - "CompileInjectorBoot", "Restore", "SetCILogging", "Test" diff --git a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj index d56faa31e..da31c9a8e 100644 --- a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj +++ b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj @@ -26,6 +26,7 @@ + From 2b2f628096f25b00994f5fc5abec1acc2eb6327e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:43:49 +0100 Subject: [PATCH 156/477] Convert ObjectTable enumerator to struct --- .../Game/ClientState/Objects/ObjectTable.cs | 40 ++----------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 84c1b5693..598daf518 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -12,8 +12,6 @@ using Dalamud.Utility; using FFXIVClientStructs.Interop; -using Microsoft.Extensions.ObjectPool; - using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager; @@ -34,8 +32,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable private readonly ClientState clientState; private readonly CachedEntry[] cachedObjectTable; - private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4]; - [ServiceManager.ServiceConstructor] private unsafe ObjectTable(ClientState clientState) { @@ -47,9 +43,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable this.cachedObjectTable = new CachedEntry[objectTableLength]; for (var i = 0; i < this.cachedObjectTable.Length; i++) this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i)); - - for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++) - this.frameworkThreadEnumerators[i] = new(this, i); } /// @@ -239,30 +232,14 @@ internal sealed partial class ObjectTable public IEnumerator GetEnumerator() { ThreadSafety.AssertMainThread(); - - // If we're on the framework thread, see if there's an already allocated enumerator available for use. - foreach (ref var x in this.frameworkThreadEnumerators.AsSpan()) - { - if (x is not null) - { - var t = x; - x = null; - t.Reset(); - return t; - } - } - - // No reusable enumerator is available; allocate a new temporary one. - return new Enumerator(this, -1); + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator, IResettable + private struct Enumerator(ObjectTable owner) : IEnumerator { - private ObjectTable? owner = owner; - private int index = -1; public IGameObject Current { get; private set; } = null!; @@ -274,7 +251,7 @@ internal sealed partial class ObjectTable if (this.index == objectTableLength) return false; - var cache = this.owner!.cachedObjectTable.AsSpan(); + var cache = owner.cachedObjectTable.AsSpan(); for (this.index++; this.index < objectTableLength; this.index++) { if (cache[this.index].Update() is { } ao) @@ -291,17 +268,6 @@ internal sealed partial class ObjectTable public void Dispose() { - if (this.owner is not { } o) - return; - - if (slotId != -1) - o.frameworkThreadEnumerators[slotId] = this; - } - - public bool TryReset() - { - this.Reset(); - return true; } } } From dd70c5b8eea1627566f0b355fc5ce7807007e803 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:36:45 +0100 Subject: [PATCH 157/477] Add struct enumerator to AetheryteList --- .../ClientState/Aetherytes/AetheryteList.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index a3d44d423..f72339ed2 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -87,10 +87,7 @@ internal sealed partial class AetheryteList /// public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - yield return this[i]; - } + return new Enumerator(this); } /// @@ -98,4 +95,30 @@ internal sealed partial class AetheryteList { return this.GetEnumerator(); } + + private struct Enumerator(AetheryteList aetheryteList) : IEnumerator + { + private int index = 0; + + public IAetheryteEntry Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == aetheryteList.Length) return false; + this.Current = aetheryteList[this.index]; + this.index++; + return true; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 520e3ea028044395925e8d73d29a1a2fb4f5410f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:37:15 +0100 Subject: [PATCH 158/477] Convert AetheryteEntry to readonly struct --- .../ClientState/Aetherytes/AetheryteEntry.cs | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs index 89dd8b8b1..e0a5df06d 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs @@ -63,47 +63,37 @@ public interface IAetheryteEntry } /// -/// Class representing an aetheryte entry available to the game. +/// This struct represents an aetheryte entry available to the game. /// -internal sealed class AetheryteEntry : IAetheryteEntry +/// Data read from the Aetheryte List. +internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry { - private readonly TeleportInfo data; - - /// - /// Initializes a new instance of the class. - /// - /// Data read from the Aetheryte List. - internal AetheryteEntry(TeleportInfo data) - { - this.data = data; - } + /// + public uint AetheryteId => data.AetheryteId; /// - public uint AetheryteId => this.data.AetheryteId; + public uint TerritoryId => data.TerritoryId; /// - public uint TerritoryId => this.data.TerritoryId; + public byte SubIndex => data.SubIndex; /// - public byte SubIndex => this.data.SubIndex; + public byte Ward => data.Ward; /// - public byte Ward => this.data.Ward; + public byte Plot => data.Plot; /// - public byte Plot => this.data.Plot; + public uint GilCost => data.GilCost; /// - public uint GilCost => this.data.GilCost; + public bool IsFavourite => data.IsFavourite; /// - public bool IsFavourite => this.data.IsFavourite; + public bool IsSharedHouse => data.IsSharedHouse; /// - public bool IsSharedHouse => this.data.IsSharedHouse; - - /// - public bool IsApartment => this.data.IsApartment; + public bool IsApartment => data.IsApartment; /// public RowRef AetheryteData => LuminaUtils.CreateRef(this.AetheryteId); From 8a9b47c7a472987c33240594fbea645727fb7dc3 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:37:35 +0100 Subject: [PATCH 159/477] Add struct enumerator to BuddyList --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 31 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 84cfd24a3..71121e54e 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -130,12 +130,35 @@ internal sealed partial class BuddyList /// public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - yield return this[i]; - } + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private struct Enumerator(BuddyList buddyList) : IEnumerator + { + private int index = 0; + + public IBuddyMember Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == buddyList.Length) return false; + this.Current = buddyList[this.index]; + this.index++; + return true; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 23e7c164d86ca1d59713f7f7f8955f2eed2b29d6 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:42:07 +0100 Subject: [PATCH 160/477] Convert BuddyMember to readonly struct --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 33 +++++----- Dalamud/Game/ClientState/Buddy/BuddyMember.cs | 60 ++++++++++++------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 71121e54e..78809f8ba 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -8,6 +8,9 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.UI; +using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; +using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; + namespace Dalamud.Game.ClientState.Buddy; /// @@ -21,7 +24,7 @@ namespace Dalamud.Game.ClientState.Buddy; #pragma warning restore SA1015 internal sealed partial class BuddyList : IServiceType, IBuddyList { - private const uint InvalidObjectID = 0xE0000000; + private const uint InvalidEntityId = 0xE0000000; [ServiceManager.ServiceDependency] private readonly ClientState clientState = Service.Get(); @@ -69,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } } - private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy; + private unsafe CSBuddy* BuddyListStruct => &UIState.Instance()->Buddy; /// public IBuddyMember? this[int index] @@ -82,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } /// - public unsafe IntPtr GetCompanionBuddyMemberAddress() + public unsafe nint GetCompanionBuddyMemberAddress() { - return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion; + return (nint)this.BuddyListStruct->CompanionInfo.Companion; } /// - public unsafe IntPtr GetPetBuddyMemberAddress() + public unsafe nint GetPetBuddyMemberAddress() { - return (IntPtr)this.BuddyListStruct->PetInfo.Pet; + return (nint)this.BuddyListStruct->PetInfo.Pet; } /// - public unsafe IntPtr GetBattleBuddyMemberAddress(int index) + public unsafe nint GetBattleBuddyMemberAddress(int index) { if (index < 0 || index >= 3) - return IntPtr.Zero; + return 0; - return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]); + return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]); } /// - public IBuddyMember? CreateBuddyMemberReference(IntPtr address) + public unsafe IBuddyMember? CreateBuddyMemberReference(nint address) { + if (address == 0) + return null; + if (this.clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) - return null; - - var buddy = new BuddyMember(address); - if (buddy.ObjectId == InvalidObjectID) + var buddy = new BuddyMember((CSBuddyMember*)address); + if (buddy.EntityId == InvalidEntityId) return null; return buddy; diff --git a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs index 393598d32..8018bafaf 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs @@ -1,20 +1,24 @@ +using System.Diagnostics.CodeAnalysis; + using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Excel; +using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; + namespace Dalamud.Game.ClientState.Buddy; /// /// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. /// -public interface IBuddyMember +public interface IBuddyMember : IEquatable { /// /// Gets the address of the buddy in memory. /// - IntPtr Address { get; } + nint Address { get; } /// /// Gets the object ID of this buddy. @@ -67,42 +71,34 @@ public interface IBuddyMember } /// -/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. +/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. /// -internal unsafe class BuddyMember : IBuddyMember +/// A pointer to the BuddyMember. +internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember { [ServiceManager.ServiceDependency] private readonly ObjectTable objectTable = Service.Get(); - /// - /// Initializes a new instance of the class. - /// - /// Buddy address. - internal BuddyMember(IntPtr address) - { - this.Address = address; - } + /// + public nint Address => (nint)ptr; /// - public IntPtr Address { get; } + public uint ObjectId => this.EntityId; /// - public uint ObjectId => this.Struct->EntityId; + public uint EntityId => ptr->EntityId; /// - public uint EntityId => this.Struct->EntityId; + public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId); /// - public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId); + public uint CurrentHP => ptr->CurrentHealth; /// - public uint CurrentHP => this.Struct->CurrentHealth; + public uint MaxHP => ptr->MaxHealth; /// - public uint MaxHP => this.Struct->MaxHealth; - - /// - public uint DataID => this.Struct->DataId; + public uint DataID => ptr->DataId; /// public RowRef MountData => LuminaUtils.CreateRef(this.DataID); @@ -113,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember /// public RowRef TrustData => LuminaUtils.CreateRef(this.DataID); - private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address; + public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y); + + public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y); + + /// + public bool Equals(IBuddyMember? other) + { + return this.EntityId == other.EntityId; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is BuddyMember fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return this.EntityId.GetHashCode(); + } } From d1bed3ebc5e5f4ec8a7104c2e718babb4b546425 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:42:31 +0100 Subject: [PATCH 161/477] Add struct enumerator to FateTable --- Dalamud/Game/ClientState/Fates/FateTable.cs | 31 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 1bf557ad5..942d1561f 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -110,12 +110,35 @@ internal sealed partial class FateTable /// public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - yield return this[i]; - } + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private struct Enumerator(FateTable fateTable) : IEnumerator + { + private int index = 0; + + public IFate Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == fateTable.Length) return false; + this.Current = fateTable[this.index]; + this.index++; + return true; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From a48eead85e9f9fb98fcaa841c34752b7dca700e2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:43:06 +0100 Subject: [PATCH 162/477] Convert Fate to readonly struct --- Dalamud/Game/ClientState/Fates/Fate.cs | 130 ++++++++------------ Dalamud/Game/ClientState/Fates/FateTable.cs | 22 ++-- 2 files changed, 59 insertions(+), 93 deletions(-) diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index 504b690c3..c40a8960e 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Dalamud.Data; @@ -6,10 +7,12 @@ using Dalamud.Memory; using Lumina.Excel; +using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext; + namespace Dalamud.Game.ClientState.Fates; /// -/// Interface representing an fate entry that can be seen in the current area. +/// Interface representing a fate entry that can be seen in the current area. /// public interface IFate : IEquatable { @@ -111,133 +114,96 @@ public interface IFate : IEquatable /// /// Gets the address of this Fate in memory. /// - IntPtr Address { get; } + nint Address { get; } } /// -/// This class represents an FFXIV Fate. +/// This struct represents a Fate. /// -internal unsafe partial class Fate +/// A pointer to the FateContext. +internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate { - /// - /// Initializes a new instance of the class. - /// - /// The address of this fate in memory. - internal Fate(IntPtr address) - { - this.Address = address; - } - /// - public IntPtr Address { get; } - - private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address; - - public static bool operator ==(Fate fate1, Fate fate2) - { - if (fate1 is null || fate2 is null) - return Equals(fate1, fate2); - - return fate1.Equals(fate2); - } - - public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2); - - /// - /// Gets a value indicating whether this Fate is still valid in memory. - /// - /// The fate to check. - /// True or false. - public static bool IsValid(Fate fate) - { - var clientState = Service.GetNullable(); - - if (fate == null || clientState == null) - return false; - - if (clientState.LocalContentId == 0) - return false; - - return true; - } - - /// - /// Gets a value indicating whether this actor is still valid in memory. - /// - /// True or false. - public bool IsValid() => IsValid(this); + public nint Address => (nint)ptr; /// - bool IEquatable.Equals(IFate other) => this.FateId == other?.FateId; - - /// - public override bool Equals(object obj) => ((IEquatable)this).Equals(obj as IFate); - - /// - public override int GetHashCode() => this.FateId.GetHashCode(); -} - -/// -/// This class represents an FFXIV Fate. -/// -internal unsafe partial class Fate : IFate -{ - /// - public ushort FateId => this.Struct->FateId; + public ushort FateId => ptr->FateId; /// public RowRef GameData => LuminaUtils.CreateRef(this.FateId); /// - public int StartTimeEpoch => this.Struct->StartTimeEpoch; + public int StartTimeEpoch => ptr->StartTimeEpoch; /// - public short Duration => this.Struct->Duration; + public short Duration => ptr->Duration; /// public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds(); /// - public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name); + public SeString Name => MemoryHelper.ReadSeString(&ptr->Name); /// - public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description); + public SeString Description => MemoryHelper.ReadSeString(&ptr->Description); /// - public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective); + public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective); /// - public FateState State => (FateState)this.Struct->State; + public FateState State => (FateState)ptr->State; /// - public byte HandInCount => this.Struct->HandInCount; + public byte HandInCount => ptr->HandInCount; /// - public byte Progress => this.Struct->Progress; + public byte Progress => ptr->Progress; /// - public bool HasBonus => this.Struct->IsBonus; + public bool HasBonus => ptr->IsBonus; /// - public uint IconId => this.Struct->IconId; + public uint IconId => ptr->IconId; /// - public byte Level => this.Struct->Level; + public byte Level => ptr->Level; /// - public byte MaxLevel => this.Struct->MaxLevel; + public byte MaxLevel => ptr->MaxLevel; /// - public Vector3 Position => this.Struct->Location; + public Vector3 Position => ptr->Location; /// - public float Radius => this.Struct->Radius; + public float Radius => ptr->Radius; /// - public uint MapIconId => this.Struct->MapIconId; + public uint MapIconId => ptr->MapIconId; /// /// Gets the territory this is located in. /// - public RowRef TerritoryType => LuminaUtils.CreateRef(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId); + public RowRef TerritoryType => LuminaUtils.CreateRef(ptr->MapMarkers[0].MapMarkerData.TerritoryTypeId); + + public static bool operator ==(Fate x, Fate y) => x.Equals(y); + + public static bool operator !=(Fate x, Fate y) => !(x == y); + + /// + public bool Equals(IFate? other) + { + return this.FateId == other.FateId; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is Fate fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return this.FateId.GetHashCode(); + } } diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 942d1561f..a6edf4a18 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -5,6 +5,7 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; +using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext; using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager; namespace Dalamud.Game.ClientState.Fates; @@ -25,7 +26,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable } /// - public unsafe IntPtr Address => (nint)CSFateManager.Instance(); + public unsafe nint Address => (nint)CSFateManager.Instance(); /// public unsafe int Length @@ -72,30 +73,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable } /// - public unsafe IntPtr GetFateAddress(int index) + public unsafe nint GetFateAddress(int index) { if (index >= this.Length) - return IntPtr.Zero; + return 0; var fateManager = CSFateManager.Instance(); if (fateManager == null) - return IntPtr.Zero; + return 0; - return (IntPtr)fateManager->Fates[index].Value; + return (nint)fateManager->Fates[index].Value; } /// - public IFate? CreateFateReference(IntPtr offset) + public unsafe IFate? CreateFateReference(IntPtr address) { - var clientState = Service.Get(); + if (address == 0) + return null; + var clientState = Service.Get(); if (clientState.LocalContentId == 0) return null; - if (offset == IntPtr.Zero) - return null; - - return new Fate(offset); + return new Fate((CSFateContext*)address); } } From d1dc81318a8aa26fbf35da9de96fba3fb369edd5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:47:55 +0100 Subject: [PATCH 163/477] Add struct enumerator to PartyList --- Dalamud/Game/ClientState/Party/PartyList.cs | 46 ++++++++++++++++----- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index a016a8211..bfd423a79 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -133,18 +133,44 @@ internal sealed partial class PartyList /// public IEnumerator GetEnumerator() { - // Normally using Length results in a recursion crash, however we know the party size via ptr. - for (var i = 0; i < this.Length; i++) - { - var member = this[i]; - - if (member == null) - break; - - yield return member; - } + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private struct Enumerator(PartyList partyList) : IEnumerator + { + private int index = 0; + + public IPartyMember Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == partyList.Length) return false; + + for (; this.index < partyList.Length; this.index++) + { + var partyMember = partyList[this.index]; + if (partyMember != null) + { + this.Current = partyMember; + return true; + } + } + + return false; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 53b94caeb7470dc7f2ca639412188f38a7f19343 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:53:03 +0100 Subject: [PATCH 164/477] Convert PartyMember to readonly struct --- Dalamud/Game/ClientState/Party/PartyList.cs | 31 ++++---- Dalamud/Game/ClientState/Party/PartyMember.cs | 79 +++++++++++-------- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index bfd423a79..ec22932ab 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -8,6 +8,7 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager; +using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember; namespace Dalamud.Game.ClientState.Party; @@ -42,20 +43,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0; /// - public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance(); + public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance(); /// - public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); + public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); /// - public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); + public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); /// public long PartyId => this.GroupManagerStruct->MainGroup.PartyId; - private static int PartyMemberSize { get; } = Marshal.SizeOf(); + private static int PartyMemberSize { get; } = Marshal.SizeOf(); - private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress; + private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress; /// public IPartyMember? this[int index] @@ -80,45 +81,45 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList } /// - public IntPtr GetPartyMemberAddress(int index) + public nint GetPartyMemberAddress(int index) { if (index < 0 || index >= GroupLength) - return IntPtr.Zero; + return 0; return this.GroupListAddress + (index * PartyMemberSize); } /// - public IPartyMember? CreatePartyMemberReference(IntPtr address) + public IPartyMember? CreatePartyMemberReference(nint address) { if (this.clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; - return new PartyMember(address); + return new PartyMember((CSPartyMember*)address); } /// - public IntPtr GetAllianceMemberAddress(int index) + public nint GetAllianceMemberAddress(int index) { if (index < 0 || index >= AllianceLength) - return IntPtr.Zero; + return 0; return this.AllianceListAddress + (index * PartyMemberSize); } /// - public IPartyMember? CreateAllianceMemberReference(IntPtr address) + public IPartyMember? CreateAllianceMemberReference(nint address) { if (this.clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; - return new PartyMember(address); + return new PartyMember((CSPartyMember*)address); } } diff --git a/Dalamud/Game/ClientState/Party/PartyMember.cs b/Dalamud/Game/ClientState/Party/PartyMember.cs index 4c738d866..c9980d9f2 100644 --- a/Dalamud/Game/ClientState/Party/PartyMember.cs +++ b/Dalamud/Game/ClientState/Party/PartyMember.cs @@ -1,26 +1,27 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; -using System.Runtime.CompilerServices; using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Memory; using Lumina.Excel; +using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember; + namespace Dalamud.Game.ClientState.Party; /// /// Interface representing a party member. /// -public interface IPartyMember +public interface IPartyMember : IEquatable { /// /// Gets the address of this party member in memory. /// - IntPtr Address { get; } + nint Address { get; } /// /// Gets a list of buffs or debuffs applied to this party member. @@ -108,69 +109,81 @@ public interface IPartyMember } /// -/// This class represents a party member in the group manager. +/// This struct represents a party member in the group manager. /// -internal unsafe class PartyMember : IPartyMember +/// A pointer to the PartyMember. +internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember { - /// - /// Initializes a new instance of the class. - /// - /// Address of the party member. - internal PartyMember(IntPtr address) - { - this.Address = address; - } + /// + public nint Address => (nint)ptr; /// - public IntPtr Address { get; } + public StatusList Statuses => new(&ptr->StatusManager); /// - public StatusList Statuses => new(&this.Struct->StatusManager); + public Vector3 Position => ptr->Position; /// - public Vector3 Position => this.Struct->Position; + public long ContentId => (long)ptr->ContentId; /// - public long ContentId => (long)this.Struct->ContentId; + public uint ObjectId => ptr->EntityId; /// - public uint ObjectId => this.Struct->EntityId; - - /// - public uint EntityId => this.Struct->EntityId; + public uint EntityId => ptr->EntityId; /// public IGameObject? GameObject => Service.Get().SearchById(this.EntityId); /// - public uint CurrentHP => this.Struct->CurrentHP; + public uint CurrentHP => ptr->CurrentHP; /// - public uint MaxHP => this.Struct->MaxHP; + public uint MaxHP => ptr->MaxHP; /// - public ushort CurrentMP => this.Struct->CurrentMP; + public ushort CurrentMP => ptr->CurrentMP; /// - public ushort MaxMP => this.Struct->MaxMP; + public ushort MaxMP => ptr->MaxMP; /// - public RowRef Territory => LuminaUtils.CreateRef(this.Struct->TerritoryType); + public RowRef Territory => LuminaUtils.CreateRef(ptr->TerritoryType); /// - public RowRef World => LuminaUtils.CreateRef(this.Struct->HomeWorld); + public RowRef World => LuminaUtils.CreateRef(ptr->HomeWorld); /// - public SeString Name => SeString.Parse(this.Struct->Name); + public SeString Name => SeString.Parse(ptr->Name); /// - public byte Sex => this.Struct->Sex; + public byte Sex => ptr->Sex; /// - public RowRef ClassJob => LuminaUtils.CreateRef(this.Struct->ClassJob); + public RowRef ClassJob => LuminaUtils.CreateRef(ptr->ClassJob); /// - public byte Level => this.Struct->Level; + public byte Level => ptr->Level; - private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address; + public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y); + + public static bool operator !=(PartyMember x, PartyMember y) => !(x == y); + + /// + public bool Equals(IPartyMember? other) + { + return this.EntityId == other.EntityId; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is PartyMember fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return this.EntityId.GetHashCode(); + } } From 7f2ed9adb6534934e6440b288fd6c753bddbe67c Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 19:01:04 +0100 Subject: [PATCH 165/477] Convert Status to readonly struct and add interface --- Dalamud/Game/ClientState/Statuses/Status.cs | 86 +++++++++++++++------ 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/Dalamud/Game/ClientState/Statuses/Status.cs b/Dalamud/Game/ClientState/Statuses/Status.cs index 2775f8f9b..160b15de5 100644 --- a/Dalamud/Game/ClientState/Statuses/Status.cs +++ b/Dalamud/Game/ClientState/Statuses/Status.cs @@ -1,61 +1,49 @@ +using System.Diagnostics.CodeAnalysis; + using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Excel; +using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status; + namespace Dalamud.Game.ClientState.Statuses; /// -/// This class represents a status effect an actor is afflicted by. +/// Interface representing a status. /// -public unsafe class Status +public interface IStatus : IEquatable { - /// - /// Initializes a new instance of the class. - /// - /// Status address. - internal Status(IntPtr address) - { - this.Address = address; - } - /// /// Gets the address of the status in memory. /// - public IntPtr Address { get; } + nint Address { get; } /// /// Gets the status ID of this status. /// - public uint StatusId => this.Struct->StatusId; + uint StatusId { get; } /// /// Gets the GameData associated with this status. /// - public RowRef GameData => LuminaUtils.CreateRef(this.Struct->StatusId); + RowRef GameData { get; } /// /// Gets the parameter value of the status. /// - public ushort Param => this.Struct->Param; - - /// - /// Gets the stack count of this status. - /// Only valid if this is a non-food status. - /// - [Obsolete($"Replaced with {nameof(Param)}", true)] - public byte StackCount => (byte)this.Struct->Param; + ushort Param { get; } /// /// Gets the time remaining of this status. /// - public float RemainingTime => this.Struct->RemainingTime; + float RemainingTime { get; } /// /// Gets the source ID of this status. /// - public uint SourceId => this.Struct->SourceObject.ObjectId; + uint SourceId { get; } /// /// Gets the source actor associated with this status. @@ -63,7 +51,55 @@ public unsafe class Status /// /// This iterates the actor table, it should be used with care. /// + IGameObject? SourceObject { get; } +} + +/// +/// This struct represents a status effect an actor is afflicted by. +/// +/// A pointer to the Status. +internal unsafe readonly struct Status(CSStatus* ptr) : IStatus +{ + /// + public nint Address => (nint)ptr; + + /// + public uint StatusId => ptr->StatusId; + + /// + public RowRef GameData => LuminaUtils.CreateRef(ptr->StatusId); + + /// + public ushort Param => ptr->Param; + + /// + public float RemainingTime => ptr->RemainingTime; + + /// + public uint SourceId => ptr->SourceObject.ObjectId; + + /// public IGameObject? SourceObject => Service.Get().SearchById(this.SourceId); - private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address; + public static bool operator ==(Status x, Status y) => x.Equals(y); + + public static bool operator !=(Status x, Status y) => !(x == y); + + /// + public bool Equals(IStatus? other) + { + return this.StatusId == other.StatusId && this.SourceId == other.SourceId && this.Param == other.Param && this.RemainingTime == other.RemainingTime; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is Status fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.StatusId, this.SourceId, this.Param, this.RemainingTime); + } } From 778c82fad2c369e59e442dab8b0a1e3eb7000373 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 19:02:50 +0100 Subject: [PATCH 166/477] Add struct enumerator to StatusList --- .../Game/ClientState/Statuses/StatusList.cs | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index a38e45ea3..50d242d33 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status; + namespace Dalamud.Game.ClientState.Statuses; /// @@ -14,7 +16,7 @@ public sealed unsafe partial class StatusList /// Initializes a new instance of the class. /// /// Address of the status list. - internal StatusList(IntPtr address) + internal StatusList(nint address) { this.Address = address; } @@ -24,14 +26,14 @@ public sealed unsafe partial class StatusList /// /// Pointer to the status list. internal unsafe StatusList(void* pointer) - : this((IntPtr)pointer) + : this((nint)pointer) { } /// /// Gets the address of the status list in memory. /// - public IntPtr Address { get; } + public nint Address { get; } /// /// Gets the amount of status effect slots the actor has. @@ -47,7 +49,7 @@ public sealed unsafe partial class StatusList /// /// Status Index. /// The status at the specified index. - public Status? this[int index] + public IStatus? this[int index] { get { @@ -64,7 +66,7 @@ public sealed unsafe partial class StatusList /// /// The address of the status list in memory. /// The status object containing the requested data. - public static StatusList? CreateStatusListReference(IntPtr address) + public static StatusList? CreateStatusListReference(nint address) { // The use case for CreateStatusListReference and CreateStatusReference to be static is so // fake status lists can be generated. Since they aren't exposed as services, it's either @@ -74,7 +76,7 @@ public sealed unsafe partial class StatusList if (clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; return new StatusList(address); @@ -85,17 +87,17 @@ public sealed unsafe partial class StatusList /// /// The address of the status effect in memory. /// The status object containing the requested data. - public static Status? CreateStatusReference(IntPtr address) + public static IStatus? CreateStatusReference(nint address) { var clientState = Service.Get(); if (clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; - return new Status(address); + return new Status((CSStatus*)address); } /// @@ -103,22 +105,22 @@ public sealed unsafe partial class StatusList /// /// The index of the status. /// The memory address of the status. - public IntPtr GetStatusAddress(int index) + public nint GetStatusAddress(int index) { if (index < 0 || index >= this.Length) - return IntPtr.Zero; + return 0; - return (IntPtr)Unsafe.AsPointer(ref this.Struct->Status[index]); + return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]); } } /// /// This collection represents the status effects an actor is afflicted by. /// -public sealed partial class StatusList : IReadOnlyCollection, ICollection +public sealed partial class StatusList : IReadOnlyCollection, ICollection { /// - int IReadOnlyCollection.Count => this.Length; + int IReadOnlyCollection.Count => this.Length; /// int ICollection.Count => this.Length; @@ -130,17 +132,9 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio object ICollection.SyncRoot => this; /// - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - var status = this[i]; - - if (status == null || status.StatusId == 0) - continue; - - yield return status; - } + return new Enumerator(this); } /// @@ -155,4 +149,39 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio index++; } } + + private struct Enumerator(StatusList statusList) : IEnumerator + { + private int index = 0; + + public IStatus Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == statusList.Length) return false; + + for (; this.index < statusList.Length; this.index++) + { + var status = statusList[this.index]; + if (status != null && status.StatusId != 0) + { + this.Current = status; + return true; + } + } + + return false; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 9d0879148c740942523da5310c5b8039cb3be707 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 19:05:25 +0100 Subject: [PATCH 167/477] Remove unused StatusEffect struct --- .../Game/ClientState/Structs/StatusEffect.cs | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 Dalamud/Game/ClientState/Structs/StatusEffect.cs diff --git a/Dalamud/Game/ClientState/Structs/StatusEffect.cs b/Dalamud/Game/ClientState/Structs/StatusEffect.cs deleted file mode 100644 index 2a60a7d3b..000000000 --- a/Dalamud/Game/ClientState/Structs/StatusEffect.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Dalamud.Game.ClientState.Structs; - -/// -/// Native memory representation of a FFXIV status effect. -/// -[StructLayout(LayoutKind.Sequential)] -public struct StatusEffect -{ - /// - /// The effect ID. - /// - public short EffectId; - - /// - /// How many stacks are present. - /// - public byte StackCount; - - /// - /// Additional parameters. - /// - public byte Param; - - /// - /// The duration remaining. - /// - public float Duration; - - /// - /// The ID of the actor that caused this effect. - /// - public int OwnerId; -} From a134c6d064d66a296730b7d45e726ef1e8082e7f Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 15 Nov 2025 00:49:06 +0100 Subject: [PATCH 168/477] api14 rollup --- .github/workflows/rollup.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml index 8bc9a3c51..8fe049ad7 100644 --- a/.github/workflows/rollup.yml +++ b/.github/workflows/rollup.yml @@ -1,8 +1,8 @@ name: Rollup changes to next version on: -# push: -# branches: -# - master + push: + branches: + - master workflow_dispatch: jobs: @@ -11,7 +11,7 @@ jobs: strategy: matrix: branches: - - net9 + - api14 defaults: run: From fea7b3676f79f7959fa9e3ee173427ec18aba48d Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 15 Nov 2025 01:07:23 +0100 Subject: [PATCH 169/477] Start correct XL binary through branch switcher, add build branch to metadata --- Dalamud/Dalamud.csproj | 9 +++++++++ .../Interface/Internal/DalamudInterface.cs | 3 ++- .../Internal/Windows/BranchSwitcherWindow.cs | 16 +++++++-------- Dalamud/Utility/Util.cs | 20 +++++++++++++++++++ 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1166c24c0..1e5f9f586 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -162,6 +162,9 @@ + + + @@ -169,6 +172,7 @@ $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", "")) $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitHash), @"\t|\n|\r", "")) + $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitBranch), @"\t|\n|\r", "")) $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", "")) $([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", "")) @@ -182,6 +186,7 @@ Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss)) + ??? ??? @@ -205,6 +210,10 @@ <_Parameter1>GitCommitCount <_Parameter2>$(CommitCount) + + <_Parameter1>GitBranch + <_Parameter2>$(Branch) + <_Parameter1>GitHashClientStructs <_Parameter2>$(CommitHashClientStructs) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 05ecff8d9..f2ffc7a4c 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1060,7 +1060,8 @@ internal class DalamudInterface : IInternalDisposableService { ImGui.PushFont(InterfaceManager.MonoFont); - ImGui.BeginMenu(Util.GetScmVersion(), false); + ImGui.BeginMenu(Util.GetBranch() ?? "???", false); + ImGui.BeginMenu($"{Util.GetScmVersion()}", false); ImGui.BeginMenu(this.FrameCount.ToString("000000"), false); ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false); diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index f7250e528..da6217aca 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -11,6 +11,8 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Networking.Http; +using Dalamud.Utility; + using Newtonsoft.Json; namespace Dalamud.Interface.Internal.Windows; @@ -44,13 +46,10 @@ public class BranchSwitcherWindow : Window this.branches = await client.GetFromJsonAsync>(BranchInfoUrl); Debug.Assert(this.branches != null, "this.branches != null"); - var config = Service.Get(); - this.selectedBranchIndex = this.branches!.Any(x => x.Key == config.DalamudBetaKind) ? - this.branches.TakeWhile(x => x.Key != config.DalamudBetaKind).Count() + var branch = Util.GetBranch(); + this.selectedBranchIndex = this.branches!.Any(x => x.Value.Track == branch) ? + this.branches.TakeWhile(x => x.Value.Track != branch).Count() : 0; - - if (this.branches.ElementAt(this.selectedBranchIndex).Value.Key != config.DalamudBetaKey) - this.selectedBranchIndex = 0; }); base.OnOpen(); @@ -88,13 +87,12 @@ public class BranchSwitcherWindow : Window var config = Service.Get(); config.DalamudBetaKind = pickedBranch.Key; config.DalamudBetaKey = pickedBranch.Value.Key; - config.QueueSave(); // If we exit immediately, we need to write out the new config now - Service.Get().ForceSave(); + config.ForceSave(); var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - var xlPath = Path.Combine(appData, "XIVLauncher", "XIVLauncher.exe"); + var xlPath = Path.Combine(appData, "XIVLauncher", "current", "XIVLauncher.exe"); if (File.Exists(xlPath)) { diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 4d578db40..50006a023 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -67,6 +67,7 @@ public static partial class Util private static string? scmVersionInternal; private static string? gitHashInternal; private static string? gitHashClientStructsInternal; + private static string? branchInternal; private static ulong moduleStartAddr; private static ulong moduleEndAddr; @@ -134,6 +135,25 @@ public static partial class Util return gitHashClientStructsInternal; } + /// + /// Gets the Dalamud branch name this version of Dalamud was built from, or null, if this is a Debug build. + /// + /// The branch name. + public static string? GetBranch() + { + if (branchInternal != null) + return branchInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + var gitBranch = attrs.FirstOrDefault(a => a.Key == "GitBranch")?.Value; + if (gitBranch == null) + return null; + + return branchInternal = gitBranch == "master" ? "release" : gitBranch; + } + /// public static unsafe string DescribeAddress(void* p) => DescribeAddress((nint)p); From 72dc094b5742748da848caf9a4b1fa05854e79bf Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 15 Nov 2025 19:41:46 +0100 Subject: [PATCH 170/477] build: 13.0.0.9 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1e5f9f586..e45ba8a1e 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.8 + 13.0.0.9 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 78ed4a2b01f2fb4f4ff86ea5eab2c7eca7e6b015 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 16 Nov 2025 15:55:35 -0800 Subject: [PATCH 171/477] feat: Dalamud RPC service A draft for a simple RPC service for Dalamud. Enables use of Dalamud URIs, to be added later. --- Dalamud.Test/Pipes/DalamudUriTests.cs | 107 +++++++++++ Dalamud/Dalamud.csproj | 1 + .../Networking/Pipes/Api/PluginLinkHandler.cs | 53 ++++++ Dalamud/Networking/Pipes/DalamudUri.cs | 102 +++++++++++ .../Pipes/Internal/ClientHelloService.cs | 94 ++++++++++ .../Pipes/Internal/LinkHandlerService.cs | 129 ++++++++++++++ Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs | 167 ++++++++++++++++++ Dalamud/Networking/Pipes/Rpc/RpcConnection.cs | 92 ++++++++++ .../Networking/Pipes/Rpc/RpcHostService.cs | 49 +++++ .../Pipes/Rpc/RpcServiceRegistry.cs | 85 +++++++++ Dalamud/Plugin/Services/IPluginLinkHandler.cs | 20 +++ Directory.Packages.props | 13 +- 12 files changed, 911 insertions(+), 1 deletion(-) create mode 100644 Dalamud.Test/Pipes/DalamudUriTests.cs create mode 100644 Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs create mode 100644 Dalamud/Networking/Pipes/DalamudUri.cs create mode 100644 Dalamud/Networking/Pipes/Internal/ClientHelloService.cs create mode 100644 Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcConnection.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcHostService.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs create mode 100644 Dalamud/Plugin/Services/IPluginLinkHandler.cs diff --git a/Dalamud.Test/Pipes/DalamudUriTests.cs b/Dalamud.Test/Pipes/DalamudUriTests.cs new file mode 100644 index 000000000..4977f3814 --- /dev/null +++ b/Dalamud.Test/Pipes/DalamudUriTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; + +using Dalamud.Networking.Pipes; +using Xunit; + +namespace Dalamud.Test.Pipes +{ + public class DalamudUriTests + { + [Theory] + [InlineData("https://www.google.com/", false)] + [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)] + public void ValidatesScheme(string uri, bool valid) + { + Action act = () => { _ = DalamudUri.FromUri(uri); }; + + var ex = Record.Exception(act); + if (valid) + { + Assert.Null(ex); + } + else + { + Assert.NotNull(ex); + Assert.IsType(ex); + } + } + + [Theory] + [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")] + [InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")] + [InlineData("dalamud://Test", "test")] + public void ExtractsNamespace(string uri, string expectedNamespace) + { + var dalamudUri = DalamudUri.FromUri(uri); + Assert.Equal(expectedNamespace, dalamudUri.Namespace); + } + + [Theory] + [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")] + [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")] + [InlineData("dalamud://foo/bar/baz", "/bar/baz")] + [InlineData("dalamud://foo/bar", "/bar")] + [InlineData("dalamud://foo/bar/", "/bar/")] + [InlineData("dalamud://foo/", "/")] + public void ExtractsPath(string uri, string expectedPath) + { + var dalamudUri = DalamudUri.FromUri(uri); + Assert.Equal(expectedPath, dalamudUri.Path); + } + + [Theory] + [InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")] + [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")] + [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")] + [InlineData("dalamud://foo/bar/baz", "/bar/baz")] + [InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")] + [InlineData("dalamud://foo/bar", "/bar")] + [InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")] + [InlineData("dalamud://foo/bar/", "/bar/")] + [InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")] + [InlineData("dalamud://foo/?cow=moo", "/?cow=moo")] + [InlineData("dalamud://foo/", "/")] + public void ExtractsData(string uri, string expectedData) + { + var dalamudUri = DalamudUri.FromUri(uri); + + Assert.Equal(expectedData, dalamudUri.Data); + } + + [Theory] + [InlineData("dalamud://foo/bar", 0)] + [InlineData("dalamud://foo/bar?cow=moo", 1)] + [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)] + [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)] + public void ExtractsQueryParams(string uri, int queryCount) + { + var dalamudUri = DalamudUri.FromUri(uri); + Assert.Equal(queryCount, dalamudUri.QueryParams.Count); + } + + [Theory] + [InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)] + [InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)] + [InlineData("dalamud://foo/bar/baz/qux/meh", 5)] + [InlineData("dalamud://foo/bar/baz/qux", 4)] + [InlineData("dalamud://foo/bar/baz", 3)] + [InlineData("dalamud://foo/bar/", 2)] + [InlineData("dalamud://foo/bar", 2)] + public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false) + { + var dalamudUri = DalamudUri.FromUri(uri); + var segments = dalamudUri.Segments; + + // First segment must always be `/` + Assert.Equal("/", segments[0]); + + Assert.Equal(segmentCount, segments.Length); + + if (finalSegmentEndsWithSlash) + { + Assert.EndsWith("/", segments.Last()); + } + } + } +} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1e5f9f586..849a5ce7f 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -81,6 +81,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs new file mode 100644 index 000000000..2c99901b4 --- /dev/null +++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs @@ -0,0 +1,53 @@ +using System.Linq; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Networking.Pipes.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +namespace Dalamud.Networking.Pipes.Api; + +/// +[PluginInterface] +[ServiceManager.ScopedService] +[ResolveVia] +public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler +{ + private readonly LinkHandlerService linkHandler; + private readonly LocalPlugin localPlugin; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin to bind this service to. + /// The central link handler. + internal PluginLinkHandler(LocalPlugin localPlugin, LinkHandlerService linkHandler) + { + this.linkHandler = linkHandler; + this.localPlugin = localPlugin; + + this.linkHandler.Register("plugin", this.HandleUri); + } + + /// + public event IPluginLinkHandler.PluginUriReceived? OnUriReceived; + + /// + public void DisposeService() + { + this.OnUriReceived = null; + this.linkHandler.Unregister("plugin", this.HandleUri); + } + + private void HandleUri(DalamudUri uri) + { + var target = uri.Path.Split("/").FirstOrDefault(); + if (target == null || !string.Equals(target, this.localPlugin.InternalName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + this.OnUriReceived?.Invoke(uri); + } +} diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Pipes/DalamudUri.cs new file mode 100644 index 000000000..03ad15af1 --- /dev/null +++ b/Dalamud/Networking/Pipes/DalamudUri.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Web; + +namespace Dalamud.Networking.Pipes; + +/// +/// A Dalamud Uri, in the format: +/// dalamud://{NAMESPACE}/{ARBITRARY} +/// +public record DalamudUri +{ + private readonly Uri rawUri; + + private DalamudUri(Uri uri) + { + if (uri.Scheme != "dalamud") + { + throw new ArgumentOutOfRangeException(nameof(uri), "URI must be of scheme dalamud."); + } + + this.rawUri = uri; + } + + /// + /// Gets the namespace that this URI should be routed to. Generally a high level component like "PluginInstaller". + /// + public string Namespace => this.rawUri.Authority; + + /// + /// Gets the raw (untargeted) path and query params for this URI. + /// + public string Data => + this.rawUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped); + + /// + /// Gets the raw (untargeted) path for this URI. + /// + public string Path => this.rawUri.AbsolutePath; + + /// + /// Gets a list of segments based on the provided Data element. + /// + public string[] Segments => this.GetDataSegments(); + + /// + /// Gets the raw query parameters for this URI, if any. + /// + public string Query => this.rawUri.Query; + + /// + /// Gets the query params (as a parsed NameValueCollection) in this URI. + /// + public NameValueCollection QueryParams => HttpUtility.ParseQueryString(this.Query); + + /// + /// Gets the fragment (if one is specified) in this URI. + /// + public string Fragment => this.rawUri.Fragment; + + /// + public override string ToString() => this.rawUri.ToString(); + + private string[] GetDataSegments() + { + // reimplementation of the System.URI#Segments, under MIT license. + var path = this.Path; + + var segments = new List(); + var current = 0; + while (current < path.Length) + { + var next = path.IndexOf('/', current); + if (next == -1) + { + next = path.Length - 1; + } + + segments.Add(path.Substring(current, (next - current) + 1)); + current = next + 1; + } + + return segments.ToArray(); + } + + /// + /// Build a DalamudURI from a given URI. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(Uri uri) + { + return new DalamudUri(uri); + } + + /// + /// Build a DalamudURI from a URI in string format. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); +} diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs new file mode 100644 index 000000000..cc06560bd --- /dev/null +++ b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs @@ -0,0 +1,94 @@ +using System.Threading.Tasks; + +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Networking.Pipes.Rpc; +using Dalamud.Utility; + +namespace Dalamud.Networking.Pipes.Internal; + +/// +/// A minimal service to respond with information about this client. +/// +[ServiceManager.EarlyLoadedService] +internal sealed class ClientHelloService : IInternalDisposableService +{ + /// + /// Initializes a new instance of the class. + /// + /// Injected host service. + [ServiceManager.ServiceConstructor] + public ClientHelloService(RpcHostService rpcHostService) + { + rpcHostService.AddMethod("hello", this.HandleHello); + } + + /// + /// Handle a hello request. + /// + /// . + /// Respond with information. + public async Task HandleHello(ClientHelloRequest request) + { + var framework = await Service.GetAsync(); + var dalamud = await Service.GetAsync(); + var clientState = await Service.GetAsync(); + + var response = await framework.RunOnFrameworkThread(() => new ClientHelloResponse + { + ApiVersion = "1.0", + DalamudVersion = Util.GetScmVersion(), + GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", + PlayerName = clientState.IsLoggedIn ? clientState.LocalPlayer?.Name.ToString() ?? "Unknown" : null, + }); + + return response; + } + + /// + public void DisposeService() + { + } +} + +/// +/// A request from a client to say hello. +/// +internal record ClientHelloRequest +{ + /// + /// Gets the API version this client is expecting. + /// + public string ApiVersion { get; init; } = string.Empty; + + /// + /// Gets the user agent of the client. + /// + public string UserAgent { get; init; } = string.Empty; +} + +/// +/// A response from Dalamud to a hello request. +/// +internal record ClientHelloResponse +{ + /// + /// Gets the API version this server has offered. + /// + public string? ApiVersion { get; init; } + + /// + /// Gets the current Dalamud version. + /// + public string? DalamudVersion { get; init; } + + /// + /// Gets the current game version. + /// + public string? GameVersion { get; init; } + + /// + /// Gets or sets the player name, or null if the player isn't logged in. + /// + public string? PlayerName { get; set; } +} diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs new file mode 100644 index 000000000..79bb1e017 --- /dev/null +++ b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs @@ -0,0 +1,129 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +using Dalamud.Logging.Internal; +using Dalamud.Networking.Pipes.Rpc; + +namespace Dalamud.Networking.Pipes.Internal; + +/// +/// A service responsible for handling Dalamud URIs and dispatching them accordingly. +/// +[ServiceManager.EarlyLoadedService] +internal class LinkHandlerService : IInternalDisposableService +{ + private readonly ModuleLog log = new("LinkHandler"); + + // key: namespace (e.g. "plugin" or "PluginInstaller") -> list of handlers + private readonly ConcurrentDictionary>> handlers + = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Initializes a new instance of the class. + /// + /// The injected RPC host service. + [ServiceManager.ServiceConstructor] + public LinkHandlerService(RpcHostService rpcHostService) + { + rpcHostService.AddMethod("handleLink", this.HandleLinkCall); + } + + /// + public void DisposeService() + { + } + + /// + /// Register a handler for a namespace. All URIs with this namespace will be dispatched to the handler. + /// + /// The namespace to use for this subscription. + /// The command handler. + public void Register(string ns, Action handler) + { + if (string.IsNullOrWhiteSpace(ns)) + throw new ArgumentNullException(nameof(ns)); + + var list = this.handlers.GetOrAdd(ns, _ => []); + lock (list) + { + list.Add(handler); + } + + this.log.Verbose("Registered handler for {Namespace}", ns); + } + + /// + /// Unregister a handler. + /// + /// The namespace to use for this subscription. + /// The command handler. + public void Unregister(string ns, Action handler) + { + if (string.IsNullOrWhiteSpace(ns)) + return; + + if (!this.handlers.TryGetValue(ns, out var list)) + return; + + lock (list) + { + list.RemoveAll(x => x == handler); + } + + if (list.Count == 0) + this.handlers.TryRemove(ns, out _); + + this.log.Verbose("Unregistered handler for {Namespace}", ns); + } + + /// + /// Dispatch a URI to matching handlers. + /// + /// The URI to parse and dispatch. + public void Dispatch(DalamudUri uri) + { + this.log.Information("Received URI: {Uri}", uri.ToString()); + + var ns = uri.Namespace; + if (!this.handlers.TryGetValue(ns, out var list)) + return; + + Action[] snapshot; + lock (list) + { + snapshot = list.ToArray(); + } + + foreach (var h in snapshot) + { + try + { + h(uri); + } + catch (Exception e) + { + this.log.Warning(e, "Link handler threw for {UriPath}", uri.Path); + } + } + } + + /// + /// The RPC-invokable link handler. + /// + /// A plain-text URI to parse. + public void HandleLinkCall(string uri) + { + if (string.IsNullOrWhiteSpace(uri)) + return; + + try + { + var du = DalamudUri.FromUri(uri); + this.Dispatch(du); + } + catch (Exception) + { + // swallow parse errors; clients shouldn't crash the host + } + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs new file mode 100644 index 000000000..07dc9d96a --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs @@ -0,0 +1,167 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc. +/// +internal class PipeRpcHost : IDisposable +{ + private readonly ModuleLog log = new("RPC/Host"); + + private readonly RpcServiceRegistry registry = new(); + private readonly CancellationTokenSource cts = new(); + private readonly ConcurrentDictionary sessions = new(); + private Task? acceptLoopTask; + + /// + /// Initializes a new instance of the class. + /// + /// The pipe name to create. + public PipeRpcHost(string? pipeName = null) + { + // Default pipe name based on current process ID for uniqueness per Dalamud instance. + this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}"; + } + + /// + /// Gets the name of the named pipe this RPC host is using. + /// + public string PipeName { get; } + + /// Adds a local object exposing RPC methods callable by clients. + /// An arbitrary service object that will be introspected to add to RPC. + public void AddService(object service) => this.registry.AddService(service); + + /// + /// Adds a standalone JSON-RPC method callable by clients. + /// + /// The name to add. + /// The delegate that acts as the handler. + public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); + + /// Starts accepting client connections. + public void Start() + { + if (this.acceptLoopTask != null) return; + this.acceptLoopTask = Task.Run(this.AcceptLoopAsync); + } + + /// Invoke an RPC request on a specific client expecting a result. + /// The client ID to invoke. + /// The method to invoke. + /// Any arguments to invoke. + /// An optional return based on the specified RPC. + /// The expected response type. + public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) + { + if (!this.sessions.TryGetValue(clientId, out var session)) + throw new KeyNotFoundException($"No client {clientId}"); + + return session.Rpc.InvokeAsync(method, arguments); + } + + /// Send a notification to all connected clients (no response expected). + /// The method name to broadcast. + /// The arguments to broadcast. + /// Returns a Task when completed. + public Task BroadcastNotifyAsync(string method, params object[] arguments) + { + var list = this.sessions.Values; + var tasks = new List(list.Count); + foreach (var s in list) + { + tasks.Add(s.Rpc.NotifyAsync(method, arguments)); + } + + return Task.WhenAll(tasks); + } + + /// + /// Gets a list of connected client IDs. + /// + /// Connected client IDs. + public IReadOnlyCollection GetClientIds() => this.sessions.Keys.AsReadOnlyCollection(); + + /// + public void Dispose() + { + this.cts.Cancel(); + this.acceptLoopTask?.Wait(1000); + + foreach (var kv in this.sessions) + { + kv.Value.Dispose(); + } + + this.sessions.Clear(); + this.cts.Dispose(); + this.log.Information("PipeRpcHost disposed ({Pipe})", this.PipeName); + GC.SuppressFinalize(this); + } + + private PipeSecurity BuildPipeSecurity() + { + var ps = new PipeSecurity(); + ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User!, PipeAccessRights.FullControl, AccessControlType.Allow)); + + return ps; + } + + private async Task AcceptLoopAsync() + { + this.log.Information("PipeRpcHost starting on pipe {Pipe}", this.PipeName); + var token = this.cts.Token; + var security = this.BuildPipeSecurity(); + + while (!token.IsCancellationRequested) + { + NamedPipeServerStream? server = null; + try + { + server = NamedPipeServerStreamAcl.Create( + this.PipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Message, + PipeOptions.Asynchronous, + 65536, + 65536, + security); + + await server.WaitForConnectionAsync(token).ConfigureAwait(false); + + var session = new RpcConnection(server, this.registry); + this.sessions.TryAdd(session.Id, session); + + this.log.Debug("RPC connection created: {Id}", session.Id); + + _ = session.Completion.ContinueWith(t => + { + this.sessions.TryRemove(session.Id, out _); + this.log.Debug("RPC connection removed: {Id}", session.Id); + }, TaskScheduler.Default); + } + catch (OperationCanceledException) + { + server?.Dispose(); + break; + } + catch (Exception ex) + { + server?.Dispose(); + this.log.Error(ex, "Error in pipe accept loop"); + await Task.Delay(500, token).ConfigureAwait(false); + } + } + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs b/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs new file mode 100644 index 000000000..8e1c3a085 --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs @@ -0,0 +1,92 @@ +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +using Serilog; +using StreamJsonRpc; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// A single RPC client session connected via named pipe. +/// +internal class RpcConnection : IDisposable +{ + private readonly NamedPipeServerStream pipe; + private readonly RpcServiceRegistry registry; + private readonly CancellationTokenSource cts = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The named pipe that this connection will handle. + /// A registry of RPC services. + public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry) + { + this.Id = Guid.CreateVersion7(); + this.pipe = pipe; + this.registry = registry; + + var formatter = new JsonMessageFormatter(); + var handler = new HeaderDelimitedMessageHandler(pipe, pipe, formatter); + + this.Rpc = new JsonRpc(handler); + this.Rpc.AllowModificationWhileListening = true; + this.Rpc.Disconnected += this.OnDisconnected; + this.registry.Attach(this.Rpc); + + this.Rpc.StartListening(); + } + + /// + /// Gets the GUID for this connection. + /// + public Guid Id { get; } + + /// + /// Gets the JsonRpc instance for this connection. + /// + public JsonRpc Rpc { get; } + + /// + /// Gets a task that's called on RPC completion. + /// + public Task Completion => this.Rpc.Completion; + + /// + public void Dispose() + { + if (!this.cts.IsCancellationRequested) + { + this.cts.Cancel(); + } + + try + { + this.Rpc.Dispose(); + } + catch (Exception ex) + { + Log.Debug(ex, "Error disposing JsonRpc for client {Id}", this.Id); + } + + try + { + this.pipe.Dispose(); + } + catch (Exception ex) + { + Log.Debug(ex, "Error disposing pipe for client {Id}", this.Id); + } + + this.cts.Dispose(); + GC.SuppressFinalize(this); + } + + private void OnDisconnected(object? sender, JsonRpcDisconnectedEventArgs e) + { + Log.Debug("RPC client {Id} disconnected: {Reason}", this.Id, e.Description); + this.registry.Detach(this.Rpc); + this.Dispose(); + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs new file mode 100644 index 000000000..78df27323 --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs @@ -0,0 +1,49 @@ +using Dalamud.Logging.Internal; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// The Dalamud service repsonsible for hosting the RPC. +/// +[ServiceManager.EarlyLoadedService] +internal class RpcHostService : IServiceType, IInternalDisposableService +{ + private readonly ModuleLog log = new("RPC"); + private readonly PipeRpcHost host; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public RpcHostService() + { + this.host = new PipeRpcHost(); + this.host.Start(); + + this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName); + } + + /// + /// Gets the RPC host to drill down. + /// + public PipeRpcHost Host => this.host; + + /// + /// Add a new service Object to the RPC host. + /// + /// The object to add. + public void AddService(object service) => this.host.AddService(service); + + /// + /// Add a new standalone method to the RPC host. + /// + /// The method name to add. + /// The handler to add. + public void AddMethod(string name, Delegate handler) => this.host.AddMethod(name, handler); + + /// + public void DisposeService() + { + this.host.Dispose(); + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs new file mode 100644 index 000000000..71037d45e --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Threading; + +using StreamJsonRpc; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session. +/// New sessions get all previously registered targets; newly added targets are attached to all active sessions. +/// +internal class RpcServiceRegistry +{ + private readonly Lock sync = new(); + private readonly List targets = []; + private readonly List<(string Name, Delegate Handler)> methods = []; + private readonly List activeRpcs = []; + + /// + /// Registers a new local RPC target object. Its public JSON-RPC methods become callable by clients. + /// Adds to the registry and attaches it to all active RPC sessions. + /// + /// The service instance containing JSON-RPC callable methods to expose. + public void AddService(object service) + { + lock (this.sync) + { + this.targets.Add(service); + foreach (var rpc in this.activeRpcs) + { + rpc.AddLocalRpcTarget(service); + } + } + } + + /// + /// Registers a new standalone JSON-RPC method. + /// + /// The name of the method to add. + /// The handler to add. + public void AddMethod(string name, Delegate handler) + { + lock (this.sync) + { + this.methods.Add((name, handler)); + foreach (var rpc in this.activeRpcs) + { + rpc.AddLocalRpcMethod(name, handler); + } + } + } + + /// + /// Attaches a JsonRpc instance to the registry so it receives all existing service targets. + /// + /// The JsonRpc instance to attach and populate with current targets. + internal void Attach(JsonRpc rpc) + { + lock (this.sync) + { + this.activeRpcs.Add(rpc); + foreach (var t in this.targets) + { + rpc.AddLocalRpcTarget(t); + } + + foreach (var m in this.methods) + { + rpc.AddLocalRpcMethod(m.Name, m.Handler); + } + } + } + + /// + /// Detaches a JsonRpc instance from the registry (e.g. when a client disconnects). + /// + /// The JsonRpc instance being detached. + internal void Detach(JsonRpc rpc) + { + lock (this.sync) + { + this.activeRpcs.Remove(rpc); + } + } +} diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs new file mode 100644 index 000000000..57f772768 --- /dev/null +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -0,0 +1,20 @@ +using Dalamud.Networking.Pipes; + +namespace Dalamud.Plugin.Services; + +/// +/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the +/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. +/// +public interface IPluginLinkHandler +{ + /// + /// A delegate containing the received URI. + /// + delegate void PluginUriReceived(DalamudUri uri); + + /// + /// The event fired when a URI targeting this plugin is received. + /// + event PluginUriReceived OnUriReceived; +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 91875e63e..903a8ee88 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,11 +3,13 @@ true false + + @@ -22,26 +24,35 @@ + + + + + + + + + @@ -54,4 +65,4 @@ - \ No newline at end of file + From 4937a2f4bd2e551669e7d158b44d0f6e681ffc1d Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 16 Nov 2025 18:14:02 -0800 Subject: [PATCH 172/477] CR changes --- .../Networking/Pipes/Api/PluginLinkHandler.cs | 4 ++- .../Pipes/Internal/LinkHandlerService.cs | 36 ++++--------------- Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs | 2 +- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 5 ++- 4 files changed, 15 insertions(+), 32 deletions(-) diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs index 2c99901b4..d8f43907c 100644 --- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs @@ -1,5 +1,6 @@ using System.Linq; +using Dalamud.Console; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Networking.Pipes.Internal; @@ -43,7 +44,8 @@ public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler private void HandleUri(DalamudUri uri) { var target = uri.Path.Split("/").FirstOrDefault(); - if (target == null || !string.Equals(target, this.localPlugin.InternalName, StringComparison.OrdinalIgnoreCase)) + var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName); + if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase)) { return; } diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs index 79bb1e017..3cc4af9f4 100644 --- a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs +++ b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Dalamud.Logging.Internal; using Dalamud.Networking.Pipes.Rpc; +using Dalamud.Utility; namespace Dalamud.Networking.Pipes.Internal; @@ -65,10 +66,7 @@ internal class LinkHandlerService : IInternalDisposableService if (!this.handlers.TryGetValue(ns, out var list)) return; - lock (list) - { - list.RemoveAll(x => x == handler); - } + list.RemoveAll(x => x == handler); if (list.Count == 0) this.handlers.TryRemove(ns, out _); @@ -85,25 +83,12 @@ internal class LinkHandlerService : IInternalDisposableService this.log.Information("Received URI: {Uri}", uri.ToString()); var ns = uri.Namespace; - if (!this.handlers.TryGetValue(ns, out var list)) + if (!this.handlers.TryGetValue(ns, out var actions)) return; - Action[] snapshot; - lock (list) + foreach (var h in actions) { - snapshot = list.ToArray(); - } - - foreach (var h in snapshot) - { - try - { - h(uri); - } - catch (Exception e) - { - this.log.Warning(e, "Link handler threw for {UriPath}", uri.Path); - } + h.InvokeSafely(uri); } } @@ -116,14 +101,7 @@ internal class LinkHandlerService : IInternalDisposableService if (string.IsNullOrWhiteSpace(uri)) return; - try - { - var du = DalamudUri.FromUri(uri); - this.Dispatch(du); - } - catch (Exception) - { - // swallow parse errors; clients shouldn't crash the host - } + var du = DalamudUri.FromUri(uri); + this.Dispatch(du); } } diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs index 07dc9d96a..ad1cc72cd 100644 --- a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs +++ b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs @@ -53,7 +53,7 @@ internal class PipeRpcHost : IDisposable public void Start() { if (this.acceptLoopTask != null) return; - this.acceptLoopTask = Task.Run(this.AcceptLoopAsync); + this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); } /// Invoke an RPC request on a specific client expecting a result. diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 57f772768..22139814d 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -1,4 +1,6 @@ -using Dalamud.Networking.Pipes; +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Networking.Pipes; namespace Dalamud.Plugin.Services; @@ -6,6 +8,7 @@ namespace Dalamud.Plugin.Services; /// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the /// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// +[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] public interface IPluginLinkHandler { /// From 19a3926051ce6aa30ac907a5fb7201536b971452 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 16 Nov 2025 21:35:33 -0800 Subject: [PATCH 173/477] Better hello message --- .../Networking/Pipes/Api/PluginLinkHandler.cs | 1 + .../Pipes/Internal/ClientHelloService.cs | 45 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs index d8f43907c..78fbb0d82 100644 --- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs @@ -6,6 +6,7 @@ using Dalamud.IoC.Internal; using Dalamud.Networking.Pipes.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +#pragma warning disable DAL_RPC namespace Dalamud.Networking.Pipes.Api; diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs index cc06560bd..9c182561e 100644 --- a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs +++ b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs @@ -1,10 +1,13 @@ using System.Threading.Tasks; +using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Networking.Pipes.Rpc; using Dalamud.Utility; +using Lumina.Excel.Sheets; + namespace Dalamud.Networking.Pipes.Internal; /// @@ -30,25 +33,49 @@ internal sealed class ClientHelloService : IInternalDisposableService /// Respond with information. public async Task HandleHello(ClientHelloRequest request) { - var framework = await Service.GetAsync(); var dalamud = await Service.GetAsync(); - var clientState = await Service.GetAsync(); - var response = await framework.RunOnFrameworkThread(() => new ClientHelloResponse + return new ClientHelloResponse { ApiVersion = "1.0", DalamudVersion = Util.GetScmVersion(), GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", - PlayerName = clientState.IsLoggedIn ? clientState.LocalPlayer?.Name.ToString() ?? "Unknown" : null, - }); - - return response; + ClientIdentifier = await this.GetClientIdentifier(), + }; } /// public void DisposeService() { } + + private async Task GetClientIdentifier() + { + var framework = await Service.GetAsync(); + var clientState = await Service.GetAsync(); + var dataManager = await Service.GetAsync(); + + var clientIdentifier = $"FFXIV Process ${Environment.ProcessId}"; + + await framework.RunOnFrameworkThread(() => + { + if (clientState.IsLoggedIn) + { + var player = clientState.LocalPlayer; + if (player != null) + { + var world = dataManager.GetExcelSheet().GetRow(player.HomeWorld.RowId); + clientIdentifier = $"Logged in as {player.Name.TextValue} @ {world.Name.ExtractText()}"; + } + } + else + { + clientIdentifier = "On login screen"; + } + }); + + return clientIdentifier; + } } /// @@ -88,7 +115,7 @@ internal record ClientHelloResponse public string? GameVersion { get; init; } /// - /// Gets or sets the player name, or null if the player isn't logged in. + /// Gets an identifier for this client. /// - public string? PlayerName { get; set; } + public string? ClientIdentifier { get; init; } } From 750fa58147b8a68cd0110b084e93f0742cc38411 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Nov 2025 12:52:07 +0000 Subject: [PATCH 174/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index f6c479b3f..0afa6b672 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit f6c479b3fa0b452b44403c8ea53d592bec415e1e +Subproject commit 0afa6b67288e5e667da74c1d3ad582e6c964644c From 64d4f7061ad2973d48f8f7cb8108dd70bd358fd1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 17 Nov 2025 19:29:48 +0100 Subject: [PATCH 175/477] Rename namespace PlayerState to Player --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 8 +++++--- Dalamud/Game/ClientState/ClientState.cs | 7 ++++--- Dalamud/Game/ClientState/Fates/Fate.cs | 3 ++- Dalamud/Game/ClientState/Fates/FateTable.cs | 5 +++-- Dalamud/Game/ClientState/Objects/ObjectTable.cs | 3 ++- Dalamud/Game/ClientState/Objects/Types/GameObject.cs | 3 ++- Dalamud/Game/ClientState/Party/PartyList.cs | 3 ++- Dalamud/Game/ClientState/Statuses/StatusList.cs | 6 ++++-- Dalamud/Game/Network/Internal/NetworkHandlers.cs | 3 ++- Dalamud/Game/{PlayerState => Player}/MentorVersion.cs | 2 +- Dalamud/Game/{PlayerState => Player}/PlayerAttribute.cs | 2 +- Dalamud/Game/{PlayerState => Player}/PlayerState.cs | 2 +- Dalamud/Game/{PlayerState => Player}/Sex.cs | 2 +- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 3 ++- .../Internal/Windows/Data/Widgets/ObjectTableWidget.cs | 2 +- Dalamud/Plugin/Services/IPlayerState.cs | 2 +- 16 files changed, 34 insertions(+), 22 deletions(-) rename Dalamud/Game/{PlayerState => Player}/MentorVersion.cs (95%) rename Dalamud/Game/{PlayerState => Player}/PlayerAttribute.cs (99%) rename Dalamud/Game/{PlayerState => Player}/PlayerState.cs (99%) rename Dalamud/Game/{PlayerState => Player}/Sex.cs (86%) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 44774a574..dbac76518 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -2,11 +2,13 @@ using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; +using Dalamud.Game.Player; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.UI; +using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; +using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; namespace Dalamud.Game.ClientState.Buddy; @@ -24,7 +26,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList private const uint InvalidObjectID = 0xE0000000; [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); [ServiceManager.ServiceConstructor] private BuddyList() @@ -69,7 +71,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } } - private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy; + private unsafe CSBuddy* BuddyListStruct => &CSUIState.Instance()->Buddy; /// public IBuddyMember? this[int index] diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 64be5cc67..caf307683 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Gui; using Dalamud.Game.Network.Internal; +using Dalamud.Game.Player; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; @@ -15,7 +16,6 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Network; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -23,6 +23,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Lumina.Excel.Sheets; using Action = System.Action; +using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; namespace Dalamud.Game.ClientState; @@ -47,7 +48,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private readonly NetworkHandlers networkHandlers = Service.Get(); [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); [ServiceManager.ServiceDependency] private readonly ObjectTable objectTable = Service.Get(); @@ -285,7 +286,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.TerritoryType = (ushort)GameMain.Instance()->CurrentTerritoryTypeId; this.MapId = AgentMap.Instance()->CurrentMapId; - this.Instance = UIState.Instance()->PublicInstance.InstanceId; + this.Instance = CSUIState.Instance()->PublicInstance.InstanceId; this.initialized = true; diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index 5a82ef0c5..f82109fd0 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -1,6 +1,7 @@ using System.Numerics; using Dalamud.Data; +using Dalamud.Game.Player; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Memory; @@ -153,7 +154,7 @@ internal unsafe partial class Fate if (fate == null) return false; - var playerState = Service.Get(); + var playerState = Service.Get(); return playerState.IsLoaded == true; } diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 2266c762d..30b0f4102 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; +using Dalamud.Game.Player; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -63,7 +64,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable if (fate == null) return false; - var playerState = Service.Get(); + var playerState = Service.Get(); return playerState.IsLoaded == true; } @@ -86,7 +87,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable if (offset == IntPtr.Zero) return null; - var playerState = Service.Get(); + var playerState = Service.Get(); if (!playerState.IsLoaded) return null; diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 0a5e900f0..b66dd4775 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Player; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -32,7 +33,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable private static int objectTableLength; [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); private readonly CachedEntry[] cachedObjectTable; diff --git a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs index c37b72961..4b331a479 100644 --- a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs +++ b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs @@ -1,6 +1,7 @@ using System.Numerics; using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.Player; using Dalamud.Game.Text.SeStringHandling; namespace Dalamud.Game.ClientState.Objects.Types; @@ -171,7 +172,7 @@ internal partial class GameObject if (actor == null) return false; - var playerState = Service.Get(); + var playerState = Service.Get(); return playerState.IsLoaded == true; } diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 0a81095c6..9618b679c 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Dalamud.Game.Player; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -25,7 +26,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList private const int AllianceLength = 20; [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); [ServiceManager.ServiceConstructor] private PartyList() diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index 04d0d822c..410ae9d7c 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Dalamud.Game.Player; + namespace Dalamud.Game.ClientState.Statuses; /// @@ -72,7 +74,7 @@ public sealed unsafe partial class StatusList // The use case for CreateStatusListReference and CreateStatusReference to be static is so // fake status lists can be generated. Since they aren't exposed as services, it's either // here or somewhere else. - var playerState = Service.Get(); + var playerState = Service.Get(); if (!playerState.IsLoaded) return null; @@ -89,7 +91,7 @@ public sealed unsafe partial class StatusList if (address == IntPtr.Zero) return null; - var playerState = Service.Get(); + var playerState = Service.Get(); if (!playerState.IsLoaded) return null; diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 2f9276cc0..6a6d73b33 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Gui; using Dalamud.Game.Network.Internal.MarketBoardUploaders; using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis; using Dalamud.Game.Network.Structures; +using Dalamud.Game.Player; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Networking.Http; @@ -268,7 +269,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService private static (ulong UploaderId, uint WorldId) GetUploaderInfo() { - var playerState = Service.Get(); + var playerState = Service.Get(); return (playerState.ContentId, playerState.CurrentWorld.RowId); } diff --git a/Dalamud/Game/PlayerState/MentorVersion.cs b/Dalamud/Game/Player/MentorVersion.cs similarity index 95% rename from Dalamud/Game/PlayerState/MentorVersion.cs rename to Dalamud/Game/Player/MentorVersion.cs index 701eda112..e856e1169 100644 --- a/Dalamud/Game/PlayerState/MentorVersion.cs +++ b/Dalamud/Game/Player/MentorVersion.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.PlayerState; +namespace Dalamud.Game.Player; /// /// Specifies the mentor certification version for a player. diff --git a/Dalamud/Game/PlayerState/PlayerAttribute.cs b/Dalamud/Game/Player/PlayerAttribute.cs similarity index 99% rename from Dalamud/Game/PlayerState/PlayerAttribute.cs rename to Dalamud/Game/Player/PlayerAttribute.cs index 4db8af107..9d9954817 100644 --- a/Dalamud/Game/PlayerState/PlayerAttribute.cs +++ b/Dalamud/Game/Player/PlayerAttribute.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.PlayerState; +namespace Dalamud.Game.Player; /// /// Represents a player's attribute. diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/Player/PlayerState.cs similarity index 99% rename from Dalamud/Game/PlayerState/PlayerState.cs rename to Dalamud/Game/Player/PlayerState.cs index c80166dd5..316b09e2f 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/Player/PlayerState.cs @@ -13,7 +13,7 @@ using Lumina.Excel.Sheets; using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; using GrandCompany = Lumina.Excel.Sheets.GrandCompany; -namespace Dalamud.Game.PlayerState; +namespace Dalamud.Game.Player; /// /// This class contains the PlayerState wrappers. diff --git a/Dalamud/Game/PlayerState/Sex.cs b/Dalamud/Game/Player/Sex.cs similarity index 86% rename from Dalamud/Game/PlayerState/Sex.cs rename to Dalamud/Game/Player/Sex.cs index e6ed6cc78..0981cb9a4 100644 --- a/Dalamud/Game/PlayerState/Sex.cs +++ b/Dalamud/Game/Player/Sex.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.PlayerState; +namespace Dalamud.Game.Player; /// /// Represents the sex of a character. diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index a4efad488..58bcdbd0b 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -8,6 +8,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Config; +using Dalamud.Game.Player; using Dalamud.Game.Text.Evaluator.Internal; using Dalamud.Game.Text.Noun; using Dalamud.Game.Text.Noun.Enums; @@ -68,7 +69,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator private readonly SheetRedirectResolver sheetRedirectResolver = Service.Get(); [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); private readonly ConcurrentDictionary, string> actStrCache = []; private readonly ConcurrentDictionary, string> objStrCache = []; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index 9a2de7261..71fb18352 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -4,7 +4,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Gui; -using Dalamud.Game.PlayerState; +using Dalamud.Game.Player; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index 1a22f58d6..425ffc963 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using Dalamud.Game.PlayerState; +using Dalamud.Game.Player; using Lumina.Excel; using Lumina.Excel.Sheets; From 7ec1de4c76073bc67ff65516c4bc7175addf269c Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 19 Oct 2025 22:45:50 +0200 Subject: [PATCH 176/477] Let IDalamudPluginInterface inherit from IServiceProvider --- Dalamud/IoC/Internal/ServiceScope.cs | 10 ++++++++-- Dalamud/Plugin/DalamudPluginInterface.cs | 6 ++++++ Dalamud/Plugin/IDalamudPluginInterface.cs | 4 +--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 98209eeb7..8b12dce0b 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -12,7 +12,7 @@ namespace Dalamud.IoC.Internal; /// /// Container enabling the creation of scoped services. /// -internal interface IServiceScope : IAsyncDisposable +internal interface IServiceScope : IServiceProvider, IAsyncDisposable { /// /// Register objects that may be injected to scoped services, @@ -57,6 +57,12 @@ internal class ServiceScopeImpl : IServiceScope /// The container this scope will use to create services. public ServiceScopeImpl(ServiceContainer container) => this.container = container; + /// + object? IServiceProvider.GetService(Type serviceType) + { + return ((IServiceProvider)this.container).GetService(serviceType); + } + /// public void RegisterPrivateScopes(params object[] scopes) { diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 8455ce164..603bed7a5 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -454,6 +454,12 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa #region Dependency Injection + /// + public object? GetService(Type serviceType) + { + return this.plugin.ServiceScope.GetService(serviceType); + } + /// public T? Create(params object[] scopedObjects) where T : class { diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index e1dd34f87..d1b6977d4 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -8,8 +8,6 @@ using System.Threading.Tasks; using Dalamud.Configuration; using Dalamud.Game.Text; using Dalamud.Game.Text.Sanitizer; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings; @@ -24,7 +22,7 @@ namespace Dalamud.Plugin; /// /// This interface acts as an interface to various objects needed to interact with Dalamud and the game. /// -public interface IDalamudPluginInterface +public interface IDalamudPluginInterface : IServiceProvider { /// /// Delegate for localization change with two-letter iso lang code. From d3c812ba6c8f25a57c4a5b7b528ee5908e1b22f6 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 19 Oct 2025 22:52:20 +0200 Subject: [PATCH 177/477] Add IDalamudService marker interface --- Dalamud/Plugin/Services/IAddonEventManager.cs | 4 ++-- Dalamud/Plugin/Services/IAddonLifecycle.cs | 4 ++-- Dalamud/Plugin/Services/IAetheryteList.cs | 4 ++-- Dalamud/Plugin/Services/IBuddyList.cs | 4 ++-- Dalamud/Plugin/Services/IChatGui.cs | 2 +- Dalamud/Plugin/Services/IClientState.cs | 2 +- Dalamud/Plugin/Services/ICommandManager.cs | 4 ++-- Dalamud/Plugin/Services/ICondition.cs | 4 ++-- Dalamud/Plugin/Services/IConsole.cs | 4 ++-- Dalamud/Plugin/Services/IContextMenu.cs | 2 +- Dalamud/Plugin/Services/IDalamudService.cs | 10 ++++++++++ Dalamud/Plugin/Services/IDataManager.cs | 2 +- Dalamud/Plugin/Services/IDtrBar.cs | 4 ++-- Dalamud/Plugin/Services/IDutyState.cs | 4 ++-- Dalamud/Plugin/Services/IFateTable.cs | 4 ++-- Dalamud/Plugin/Services/IFlyTextGui.cs | 4 ++-- Dalamud/Plugin/Services/IFramework.cs | 4 ++-- Dalamud/Plugin/Services/IGameConfig.cs | 4 ++-- Dalamud/Plugin/Services/IGameGui.cs | 2 +- Dalamud/Plugin/Services/IGameInteropProvider.cs | 4 ++-- Dalamud/Plugin/Services/IGameInventory.cs | 4 ++-- Dalamud/Plugin/Services/IGameLifecycle.cs | 4 ++-- Dalamud/Plugin/Services/IGameNetwork.cs | 4 ++-- Dalamud/Plugin/Services/IGamepadState.cs | 4 ++-- Dalamud/Plugin/Services/IJobGauges.cs | 4 ++-- Dalamud/Plugin/Services/IKeyState.cs | 4 ++-- Dalamud/Plugin/Services/IMarketBoard.cs | 4 ++-- Dalamud/Plugin/Services/INamePlateGui.cs | 2 +- Dalamud/Plugin/Services/INotificationManager.cs | 2 +- Dalamud/Plugin/Services/IObjectTable.cs | 2 +- Dalamud/Plugin/Services/IPartyFinderGui.cs | 4 ++-- Dalamud/Plugin/Services/IPartyList.cs | 4 ++-- Dalamud/Plugin/Services/IPluginLog.cs | 4 ++-- Dalamud/Plugin/Services/ISeStringEvaluator.cs | 2 +- Dalamud/Plugin/Services/ISigScanner.cs | 4 +++- Dalamud/Plugin/Services/ITargetManager.cs | 5 +++-- Dalamud/Plugin/Services/ITextureProvider.cs | 2 +- Dalamud/Plugin/Services/ITextureReadbackProvider.cs | 3 +-- .../Plugin/Services/ITextureSubstitutionProvider.cs | 4 ++-- Dalamud/Plugin/Services/ITitleScreenMenu.cs | 4 ++-- Dalamud/Plugin/Services/IToastGui.cs | 4 ++-- 41 files changed, 81 insertions(+), 69 deletions(-) create mode 100644 Dalamud/Plugin/Services/IDalamudService.cs diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index c6499e4e2..6b7b1166e 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.Addon.Events; +using Dalamud.Game.Addon.Events; using Dalamud.Game.Addon.Events.EventDataTypes; namespace Dalamud.Plugin.Services; @@ -6,7 +6,7 @@ namespace Dalamud.Plugin.Services; /// /// Service provider for addon event management. /// -public interface IAddonEventManager +public interface IAddonEventManager : IDalamudService { /// /// Delegate to be called when an event is received. diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index ebf629b85..1269b13dc 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.InteropServices; using Dalamud.Game.Addon.Lifecycle; @@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services; /// /// This class provides events for in-game addon lifecycles. /// -public interface IAddonLifecycle +public interface IAddonLifecycle : IDalamudService { /// /// Delegate for receiving addon lifecycle event messages. diff --git a/Dalamud/Plugin/Services/IAetheryteList.cs b/Dalamud/Plugin/Services/IAetheryteList.cs index 88c2ff616..58b82ebf6 100644 --- a/Dalamud/Plugin/Services/IAetheryteList.cs +++ b/Dalamud/Plugin/Services/IAetheryteList.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.ClientState.Aetherytes; @@ -7,7 +7,7 @@ namespace Dalamud.Plugin.Services; /// /// This collection represents the list of available Aetherytes in the Teleport window. /// -public interface IAetheryteList : IReadOnlyCollection +public interface IAetheryteList : IDalamudService, IReadOnlyCollection { /// /// Gets the amount of Aetherytes the local player has unlocked. diff --git a/Dalamud/Plugin/Services/IBuddyList.cs b/Dalamud/Plugin/Services/IBuddyList.cs index 77c0b9c17..8d3790b6d 100644 --- a/Dalamud/Plugin/Services/IBuddyList.cs +++ b/Dalamud/Plugin/Services/IBuddyList.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.ClientState.Buddy; @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// This collection represents the buddies present in your squadron or trust party. /// It does not include the local player. /// -public interface IBuddyList : IReadOnlyCollection +public interface IBuddyList : IDalamudService, IReadOnlyCollection { /// /// Gets the amount of battle buddies the local player has. diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs index c474ca386..572ac6c95 100644 --- a/Dalamud/Plugin/Services/IChatGui.cs +++ b/Dalamud/Plugin/Services/IChatGui.cs @@ -10,7 +10,7 @@ namespace Dalamud.Plugin.Services; /// /// This class handles interacting with the native chat UI. /// -public interface IChatGui +public interface IChatGui : IDalamudService { /// /// A delegate type used with the event. diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 36bf2e296..2555b3b30 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// /// This class represents the state of the game client at the time of access. /// -public interface IClientState +public interface IClientState : IDalamudService { /// /// A delegate type used for the event. diff --git a/Dalamud/Plugin/Services/ICommandManager.cs b/Dalamud/Plugin/Services/ICommandManager.cs index a6bc4763f..46138cd71 100644 --- a/Dalamud/Plugin/Services/ICommandManager.cs +++ b/Dalamud/Plugin/Services/ICommandManager.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using Dalamud.Game.Command; @@ -7,7 +7,7 @@ namespace Dalamud.Plugin.Services; /// /// This class manages registered in-game slash commands. /// -public interface ICommandManager +public interface ICommandManager : IDalamudService { /// /// Gets a read-only list of all registered commands. diff --git a/Dalamud/Plugin/Services/ICondition.cs b/Dalamud/Plugin/Services/ICondition.cs index 4ea9e7f76..c37117f3c 100644 --- a/Dalamud/Plugin/Services/ICondition.cs +++ b/Dalamud/Plugin/Services/ICondition.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.ClientState.Conditions; @@ -7,7 +7,7 @@ namespace Dalamud.Plugin.Services; /// /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. /// -public interface ICondition +public interface ICondition : IDalamudService { /// /// A delegate type used with the event. diff --git a/Dalamud/Plugin/Services/IConsole.cs b/Dalamud/Plugin/Services/IConsole.cs index 0b6832efb..be920a5c9 100644 --- a/Dalamud/Plugin/Services/IConsole.cs +++ b/Dalamud/Plugin/Services/IConsole.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Dalamud.Console; @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// Provides functions to register console commands and variables. /// [Experimental("Dalamud001")] -public interface IConsole +public interface IConsole : IDalamudService { /// /// Gets this plugin's namespace prefix, derived off its internal name. diff --git a/Dalamud/Plugin/Services/IContextMenu.cs b/Dalamud/Plugin/Services/IContextMenu.cs index 02f773441..ed99f595e 100644 --- a/Dalamud/Plugin/Services/IContextMenu.cs +++ b/Dalamud/Plugin/Services/IContextMenu.cs @@ -5,7 +5,7 @@ namespace Dalamud.Plugin.Services; /// /// This class provides methods for interacting with the game's context menu. /// -public interface IContextMenu +public interface IContextMenu : IDalamudService { /// /// A delegate type used for the event. diff --git a/Dalamud/Plugin/Services/IDalamudService.cs b/Dalamud/Plugin/Services/IDalamudService.cs new file mode 100644 index 000000000..1472b27da --- /dev/null +++ b/Dalamud/Plugin/Services/IDalamudService.cs @@ -0,0 +1,10 @@ +namespace Dalamud.Plugin.Services; + +/// +/// Marker interface for Dalamud services. +/// +/// +/// This interface is implemented by all services provided through Dalamud's +/// dependency injection system. +/// +public interface IDalamudService; diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index 65c51a9fb..474d34ecb 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -14,7 +14,7 @@ namespace Dalamud.Plugin.Services; /// /// This class provides data for Dalamud-internal features, but can also be used by plugins if needed. /// -public interface IDataManager +public interface IDataManager : IDalamudService { /// /// Gets the current game client language. diff --git a/Dalamud/Plugin/Services/IDtrBar.cs b/Dalamud/Plugin/Services/IDtrBar.cs index 8ab34c6f2..a24327e23 100644 --- a/Dalamud/Plugin/Services/IDtrBar.cs +++ b/Dalamud/Plugin/Services/IDtrBar.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text.SeStringHandling; @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// /// Class used to interface with the server info bar. /// -public interface IDtrBar +public interface IDtrBar : IDalamudService { /// /// Gets a read-only copy of the list of all DTR bar entries. diff --git a/Dalamud/Plugin/Services/IDutyState.cs b/Dalamud/Plugin/Services/IDutyState.cs index 3d49f68cb..9ad2e3c24 100644 --- a/Dalamud/Plugin/Services/IDutyState.cs +++ b/Dalamud/Plugin/Services/IDutyState.cs @@ -1,9 +1,9 @@ -namespace Dalamud.Plugin.Services; +namespace Dalamud.Plugin.Services; /// /// This class represents the state of the currently occupied duty. /// -public interface IDutyState +public interface IDutyState : IDalamudService { /// /// Event that gets fired when the duty starts. diff --git a/Dalamud/Plugin/Services/IFateTable.cs b/Dalamud/Plugin/Services/IFateTable.cs index d10141050..3392d8e23 100644 --- a/Dalamud/Plugin/Services/IFateTable.cs +++ b/Dalamud/Plugin/Services/IFateTable.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.ClientState.Fates; @@ -7,7 +7,7 @@ namespace Dalamud.Plugin.Services; /// /// This collection represents the currently available Fate events. /// -public interface IFateTable : IReadOnlyCollection +public interface IFateTable : IDalamudService, IReadOnlyCollection { /// /// Gets the address of the Fate table. diff --git a/Dalamud/Plugin/Services/IFlyTextGui.cs b/Dalamud/Plugin/Services/IFlyTextGui.cs index 04fae351d..6c0e40fd6 100644 --- a/Dalamud/Plugin/Services/IFlyTextGui.cs +++ b/Dalamud/Plugin/Services/IFlyTextGui.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.Gui.FlyText; +using Dalamud.Game.Gui.FlyText; using Dalamud.Game.Text.SeStringHandling; namespace Dalamud.Plugin.Services; @@ -6,7 +6,7 @@ namespace Dalamud.Plugin.Services; /// /// This class facilitates interacting with and creating native in-game "fly text". /// -public interface IFlyTextGui +public interface IFlyTextGui : IDalamudService { /// /// The delegate defining the type for the FlyText event. diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs index f1a4b6906..3524ca668 100644 --- a/Dalamud/Plugin/Services/IFramework.cs +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -24,7 +24,7 @@ namespace Dalamud.Plugin.Services; /// See to see the difference in behaviors, and how would a misuse of these /// functions result in a deadlock. /// -public interface IFramework +public interface IFramework : IDalamudService { /// /// A delegate type used with the event. diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index 5d8378659..10883c6d1 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using Dalamud.Game.Config; using Dalamud.Plugin.Internal.Types; @@ -17,7 +17,7 @@ namespace Dalamud.Plugin.Services; /// If property access from the plugin constructor is desired, do the value retrieval asynchronously via /// ; do not wait for the result right away. /// -public interface IGameConfig +public interface IGameConfig : IDalamudService { /// /// Event which is fired when any game config option is changed. diff --git a/Dalamud/Plugin/Services/IGameGui.cs b/Dalamud/Plugin/Services/IGameGui.cs index 6c2e0083e..933252ff4 100644 --- a/Dalamud/Plugin/Services/IGameGui.cs +++ b/Dalamud/Plugin/Services/IGameGui.cs @@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services; /// /// A class handling many aspects of the in-game UI. /// -public unsafe interface IGameGui +public unsafe interface IGameGui : IDalamudService { /// /// Event which is fired when the game UI hiding is toggled. diff --git a/Dalamud/Plugin/Services/IGameInteropProvider.cs b/Dalamud/Plugin/Services/IGameInteropProvider.cs index 99e36c7ed..645d70ac6 100644 --- a/Dalamud/Plugin/Services/IGameInteropProvider.cs +++ b/Dalamud/Plugin/Services/IGameInteropProvider.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using Dalamud.Hooking; using Dalamud.Utility.Signatures; @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// /// Service responsible for the creation of hooks. /// -public interface IGameInteropProvider +public interface IGameInteropProvider : IDalamudService { /// /// Available hooking backends. diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 0dff1ff03..cba1c9872 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.Inventory; using Dalamud.Game.Inventory.InventoryEventArgTypes; @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// /// This class provides events for the in-game inventory. /// -public interface IGameInventory +public interface IGameInventory : IDalamudService { /// /// Delegate function to be called when inventories have been changed. diff --git a/Dalamud/Plugin/Services/IGameLifecycle.cs b/Dalamud/Plugin/Services/IGameLifecycle.cs index caa64ed23..8fae3fc0e 100644 --- a/Dalamud/Plugin/Services/IGameLifecycle.cs +++ b/Dalamud/Plugin/Services/IGameLifecycle.cs @@ -1,11 +1,11 @@ -using System.Threading; +using System.Threading; namespace Dalamud.Plugin.Services; /// /// Class offering cancellation tokens for common gameplay events. /// -public interface IGameLifecycle +public interface IGameLifecycle : IDalamudService { /// /// Gets a token that is cancelled when Dalamud is unloading. diff --git a/Dalamud/Plugin/Services/IGameNetwork.cs b/Dalamud/Plugin/Services/IGameNetwork.cs index 969176da7..4abf20834 100644 --- a/Dalamud/Plugin/Services/IGameNetwork.cs +++ b/Dalamud/Plugin/Services/IGameNetwork.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.Network; +using Dalamud.Game.Network; namespace Dalamud.Plugin.Services; @@ -6,7 +6,7 @@ namespace Dalamud.Plugin.Services; /// This class handles interacting with game network events. /// [Obsolete("Will be removed in a future release. Use packet handler hooks instead.", true)] -public interface IGameNetwork +public interface IGameNetwork : IDalamudService { // TODO(v9): we shouldn't be passing pointers to the actual data here diff --git a/Dalamud/Plugin/Services/IGamepadState.cs b/Dalamud/Plugin/Services/IGamepadState.cs index 2816c927e..bdb07b91b 100644 --- a/Dalamud/Plugin/Services/IGamepadState.cs +++ b/Dalamud/Plugin/Services/IGamepadState.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.GamePad; @@ -10,7 +10,7 @@ namespace Dalamud.Plugin.Services; /// /// Will block game's gamepad input if is set. /// -public interface IGamepadState +public interface IGamepadState : IDalamudService { /// /// Gets the pointer to the current instance of the GamepadInput struct. diff --git a/Dalamud/Plugin/Services/IJobGauges.cs b/Dalamud/Plugin/Services/IJobGauges.cs index 4489a7be7..3313de7f6 100644 --- a/Dalamud/Plugin/Services/IJobGauges.cs +++ b/Dalamud/Plugin/Services/IJobGauges.cs @@ -1,11 +1,11 @@ -using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Plugin.Services; /// /// This class converts in-memory Job gauge data to structs. /// -public interface IJobGauges +public interface IJobGauges : IDalamudService { /// /// Gets the address of the JobGauge data. diff --git a/Dalamud/Plugin/Services/IKeyState.cs b/Dalamud/Plugin/Services/IKeyState.cs index de78978ca..06d6c9b49 100644 --- a/Dalamud/Plugin/Services/IKeyState.cs +++ b/Dalamud/Plugin/Services/IKeyState.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.ClientState.Keys; @@ -16,7 +16,7 @@ namespace Dalamud.Plugin.Services; /// index & 2 = key up (ephemeral). /// index & 3 = short key press (ephemeral). /// -public interface IKeyState +public interface IKeyState : IDalamudService { /// /// Get or set the key-pressed state for a given vkCode. diff --git a/Dalamud/Plugin/Services/IMarketBoard.cs b/Dalamud/Plugin/Services/IMarketBoard.cs index 3fded6987..0bdfad175 100644 --- a/Dalamud/Plugin/Services/IMarketBoard.cs +++ b/Dalamud/Plugin/Services/IMarketBoard.cs @@ -1,11 +1,11 @@ -using Dalamud.Game.Network.Structures; +using Dalamud.Game.Network.Structures; namespace Dalamud.Plugin.Services; /// /// Provides access to market board related events as the client receives/sends them. /// -public interface IMarketBoard +public interface IMarketBoard : IDalamudService { /// /// A delegate type used with the event. diff --git a/Dalamud/Plugin/Services/INamePlateGui.cs b/Dalamud/Plugin/Services/INamePlateGui.cs index eb2579bae..b58b5b7d0 100644 --- a/Dalamud/Plugin/Services/INamePlateGui.cs +++ b/Dalamud/Plugin/Services/INamePlateGui.cs @@ -7,7 +7,7 @@ namespace Dalamud.Plugin.Services; /// /// Class used to modify the data used when rendering nameplates. /// -public interface INamePlateGui +public interface INamePlateGui : IDalamudService { /// /// The delegate used for receiving nameplate update events. diff --git a/Dalamud/Plugin/Services/INotificationManager.cs b/Dalamud/Plugin/Services/INotificationManager.cs index 7d9ccd0b0..6d9dbf584 100644 --- a/Dalamud/Plugin/Services/INotificationManager.cs +++ b/Dalamud/Plugin/Services/INotificationManager.cs @@ -3,7 +3,7 @@ using Dalamud.Interface.ImGuiNotification; namespace Dalamud.Plugin.Services; /// Manager for notifications provided by Dalamud using ImGui. -public interface INotificationManager +public interface INotificationManager : IDalamudService { /// Adds a notification. /// The new notification. diff --git a/Dalamud/Plugin/Services/IObjectTable.cs b/Dalamud/Plugin/Services/IObjectTable.cs index 36cd72ebe..be8e50dea 100644 --- a/Dalamud/Plugin/Services/IObjectTable.cs +++ b/Dalamud/Plugin/Services/IObjectTable.cs @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// /// This collection represents the currently spawned FFXIV game objects. /// -public interface IObjectTable : IEnumerable +public interface IObjectTable : IDalamudService, IEnumerable { /// /// Gets the address of the object table. diff --git a/Dalamud/Plugin/Services/IPartyFinderGui.cs b/Dalamud/Plugin/Services/IPartyFinderGui.cs index fb7a49acd..d9b14baed 100644 --- a/Dalamud/Plugin/Services/IPartyFinderGui.cs +++ b/Dalamud/Plugin/Services/IPartyFinderGui.cs @@ -1,11 +1,11 @@ -using Dalamud.Game.Gui.PartyFinder.Types; +using Dalamud.Game.Gui.PartyFinder.Types; namespace Dalamud.Plugin.Services; /// /// This class handles interacting with the native PartyFinder window. /// -public interface IPartyFinderGui +public interface IPartyFinderGui : IDalamudService { /// /// Event type fired each time the game receives an individual Party Finder listing. diff --git a/Dalamud/Plugin/Services/IPartyList.cs b/Dalamud/Plugin/Services/IPartyList.cs index b046f36db..1af3fa962 100644 --- a/Dalamud/Plugin/Services/IPartyList.cs +++ b/Dalamud/Plugin/Services/IPartyList.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.ClientState.Party; @@ -7,7 +7,7 @@ namespace Dalamud.Plugin.Services; /// /// This collection represents the actors present in your party or alliance. /// -public interface IPartyList : IReadOnlyCollection +public interface IPartyList : IDalamudService, IReadOnlyCollection { /// /// Gets the amount of party members the local player has. diff --git a/Dalamud/Plugin/Services/IPluginLog.cs b/Dalamud/Plugin/Services/IPluginLog.cs index 38406fd91..38786f0d2 100644 --- a/Dalamud/Plugin/Services/IPluginLog.cs +++ b/Dalamud/Plugin/Services/IPluginLog.cs @@ -1,4 +1,4 @@ -using Serilog; +using Serilog; using Serilog.Events; #pragma warning disable CS1573 // See https://github.com/dotnet/roslyn/issues/40325 @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// /// An opinionated service to handle logging for plugins. /// -public interface IPluginLog +public interface IPluginLog : IDalamudService { /// /// Gets a Serilog ILogger instance for this plugin. This is the entrypoint for plugins that wish to use more diff --git a/Dalamud/Plugin/Services/ISeStringEvaluator.cs b/Dalamud/Plugin/Services/ISeStringEvaluator.cs index 4efc29e3e..8ab7adad1 100644 --- a/Dalamud/Plugin/Services/ISeStringEvaluator.cs +++ b/Dalamud/Plugin/Services/ISeStringEvaluator.cs @@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services; /// /// Defines a service for retrieving localized text for various in-game entities. /// -public interface ISeStringEvaluator +public interface ISeStringEvaluator : IDalamudService { /// /// Evaluates macros in a . diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs index ac0f2c55f..fbbd8b05a 100644 --- a/Dalamud/Plugin/Services/ISigScanner.cs +++ b/Dalamud/Plugin/Services/ISigScanner.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; +using Dalamud.Plugin.Services; + namespace Dalamud.Game; /// /// A SigScanner facilitates searching for memory signatures in a given ProcessModule. /// -public interface ISigScanner +public interface ISigScanner : IDalamudService { /// /// Gets a value indicating whether the search on this module is performed on a copy. diff --git a/Dalamud/Plugin/Services/ITargetManager.cs b/Dalamud/Plugin/Services/ITargetManager.cs index 5ba9f390e..9c9fce550 100644 --- a/Dalamud/Plugin/Services/ITargetManager.cs +++ b/Dalamud/Plugin/Services/ITargetManager.cs @@ -1,11 +1,12 @@ -using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; namespace Dalamud.Game.ClientState.Objects; /// /// Get and set various kinds of targets for the player. /// -public interface ITargetManager +public interface ITargetManager : IDalamudService { /// /// Gets or sets the current target. diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index a8ad76995..7cd1b7c86 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -32,7 +32,7 @@ namespace Dalamud.Plugin.Services; /// . /// /// -public interface ITextureProvider +public interface ITextureProvider : IDalamudService { /// Creates an empty texture. /// Texture specifications. diff --git a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs index 3d2894355..00b684cbb 100644 --- a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs +++ b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs @@ -3,14 +3,13 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; namespace Dalamud.Plugin.Services; /// Service that grants you to read instances of . -public interface ITextureReadbackProvider +public interface ITextureReadbackProvider : IDalamudService { /// Gets the raw data of a texture wrap. /// The source texture wrap. diff --git a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs index 371fbaf0f..dcd1b00cc 100644 --- a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs +++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Dalamud.Plugin.Services; /// /// Service that grants you the ability to replace texture data that is to be loaded by Dalamud. /// -public interface ITextureSubstitutionProvider +public interface ITextureSubstitutionProvider : IDalamudService { /// /// Delegate describing a function that may be used to intercept and replace texture data. diff --git a/Dalamud/Plugin/Services/ITitleScreenMenu.cs b/Dalamud/Plugin/Services/ITitleScreenMenu.cs index 9f7b17ea3..50bae62a1 100644 --- a/Dalamud/Plugin/Services/ITitleScreenMenu.cs +++ b/Dalamud/Plugin/Services/ITitleScreenMenu.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Interface; using Dalamud.Interface.Textures; @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// /// Interface for class responsible for managing elements in the title screen menu. /// -public interface ITitleScreenMenu +public interface ITitleScreenMenu : IDalamudService { /// /// Gets the list of read only entries in the title screen menu. diff --git a/Dalamud/Plugin/Services/IToastGui.cs b/Dalamud/Plugin/Services/IToastGui.cs index ef83e95ac..c472cbb1f 100644 --- a/Dalamud/Plugin/Services/IToastGui.cs +++ b/Dalamud/Plugin/Services/IToastGui.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.Gui.Toast; +using Dalamud.Game.Gui.Toast; using Dalamud.Game.Text.SeStringHandling; namespace Dalamud.Plugin.Services; @@ -6,7 +6,7 @@ namespace Dalamud.Plugin.Services; /// /// This class facilitates interacting with and creating native toast windows. /// -public interface IToastGui +public interface IToastGui : IDalamudService { /// /// A delegate type used when a normal toast window appears. From 46dee9a483fd886b453be35a3cb182823ecc7e60 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 19 Oct 2025 22:54:34 +0200 Subject: [PATCH 178/477] Inherit documentation in DalamudPluginInterface --- Dalamud/Plugin/DalamudPluginInterface.cs | 196 ++++++----------------- 1 file changed, 53 insertions(+), 143 deletions(-) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 603bed7a5..6fd9064b6 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -86,126 +86,73 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved; } - /// - /// Event that gets fired when loc is changed - /// + /// public event IDalamudPluginInterface.LanguageChangedDelegate? LanguageChanged; - /// - /// Event that is fired when the active list of plugins is changed. - /// + /// public event IDalamudPluginInterface.ActivePluginsChangedDelegate? ActivePluginsChanged; - /// - /// Gets the reason this plugin was loaded. - /// + /// public PluginLoadReason Reason { get; } - /// - /// Gets a value indicating whether auto-updates have already completed this session. - /// + /// public bool IsAutoUpdateComplete => Service.GetNullable()?.IsAutoUpdateComplete ?? false; - /// - /// Gets the repository from which this plugin was installed. - /// - /// If a plugin was installed from the official/main repository, this will return the value of - /// . Developer plugins will return the value of - /// . - /// + /// public string SourceRepository { get; } - /// - /// Gets the current internal plugin name. - /// + /// public string InternalName => this.plugin.InternalName; - /// - /// Gets the plugin's manifest. - /// + /// public IPluginManifest Manifest => this.plugin.Manifest; - /// - /// Gets a value indicating whether this is a dev plugin. - /// + /// public bool IsDev => this.plugin.IsDev; - /// - /// Gets a value indicating whether this is a testing release of a plugin. - /// - /// - /// Dev plugins have undefined behavior for this value, but can be expected to return false. - /// + /// public bool IsTesting { get; } - /// - /// Gets the time that this plugin was loaded. - /// + /// public DateTime LoadTime { get; } - /// - /// Gets the UTC time that this plugin was loaded. - /// + /// public DateTime LoadTimeUTC { get; } - /// - /// Gets the timespan delta from when this plugin was loaded. - /// + /// public TimeSpan LoadTimeDelta => DateTime.Now - this.LoadTime; - /// - /// Gets the directory Dalamud assets are stored in. - /// + /// public DirectoryInfo DalamudAssetDirectory => Service.Get().AssetDirectory; - /// - /// Gets the location of your plugin assembly. - /// + /// public FileInfo AssemblyLocation => this.plugin.DllFile; - /// - /// Gets the directory your plugin configurations are stored in. - /// + /// public DirectoryInfo ConfigDirectory => new(this.GetPluginConfigDirectory()); - /// - /// Gets the config file of your plugin. - /// + /// public FileInfo ConfigFile => this.configs.GetConfigFile(this.plugin.InternalName); - /// - /// Gets the instance which allows you to draw UI into the game via ImGui draw calls. - /// + /// public IUiBuilder UiBuilder { get; private set; } - /// - /// Gets a value indicating whether Dalamud is running in Debug mode or the /xldev menu is open. This can occur on release builds. - /// + /// public bool IsDevMenuOpen => Service.GetNullable() is { IsDevMenuOpen: true }; // Can be null during boot - /// - /// Gets a value indicating whether a debugger is attached. - /// + /// public bool IsDebugging => Debugger.IsAttached; - /// - /// Gets the current UI language in two-letter iso format. - /// + /// public string UiLanguage { get; private set; } - /// - /// Gets serializer class with functions to remove special characters from strings. - /// + /// public ISanitizer Sanitizer { get; } - /// - /// Gets the chat type used by default for plugin messages. - /// + /// public XivChatType GeneralChatType { get; private set; } - /// - /// Gets a list of installed plugins along with their current state. - /// + /// public IEnumerable InstalledPlugins => Service.Get().InstalledPlugins.Select(p => new ExposedPlugin(p)); @@ -214,12 +161,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// internal UiBuilder LocalUiBuilder => this.uiBuilder; - /// - /// Opens the , with an optional search term. - /// - /// The page to open the installer to. Defaults to the "All Plugins" page. - /// An optional search text to input in the search box. - /// Returns false if the DalamudInterface was null. + /// public bool OpenPluginInstallerTo(PluginInstallerOpenKind openTo = PluginInstallerOpenKind.AllPlugins, string? searchText = null) { var dalamudInterface = Service.GetNullable(); // Can be null during boot @@ -234,12 +176,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa return true; } - /// - /// Opens the , with an optional search term. - /// - /// The tab to open the settings to. Defaults to the "General" tab. - /// An optional search text to input in the search box. - /// Returns false if the DalamudInterface was null. + /// public bool OpenDalamudSettingsTo(SettingsOpenKind openTo = SettingsOpenKind.General, string? searchText = null) { var dalamudInterface = Service.GetNullable(); // Can be null during boot @@ -254,10 +191,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa return true; } - /// - /// Opens the dev menu bar. - /// - /// Returns false if the DalamudInterface was null. + /// public bool OpenDeveloperMenu() { var dalamudInterface = Service.GetNullable(); // Can be null during boot @@ -296,102 +230,91 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa #region IPC - /// + /// public T GetOrCreateData(string tag, Func dataGenerator) where T : class => Service.Get().GetOrCreateData(tag, dataGenerator); - /// + /// public void RelinquishData(string tag) => Service.Get().RelinquishData(tag); - /// + /// public bool TryGetData(string tag, [NotNullWhen(true)] out T? data) where T : class => Service.Get().TryGetData(tag, out data); - /// + /// public T? GetData(string tag) where T : class => Service.Get().GetData(tag); - /// - /// Gets an IPC provider. - /// - /// The return type for funcs. Use object if this is unused. - /// The name of the IPC registration. - /// An IPC provider. - /// This is thrown when the requested types do not match the previously registered types are different. + /// public ICallGateProvider GetIpcProvider(string name) => new CallGatePubSub(name); - /// + /// public ICallGateProvider GetIpcProvider(string name) => new CallGatePubSub(name); - /// + /// public ICallGateProvider GetIpcProvider(string name) => new CallGatePubSub(name); - /// + /// public ICallGateProvider GetIpcProvider(string name) => new CallGatePubSub(name); - /// + /// public ICallGateProvider GetIpcProvider(string name) => new CallGatePubSub(name); - /// + /// public ICallGateProvider GetIpcProvider(string name) => new CallGatePubSub(name); - /// + /// public ICallGateProvider GetIpcProvider(string name) => new CallGatePubSub(name); - /// + /// public ICallGateProvider GetIpcProvider(string name) => new CallGatePubSub(name); - /// + /// public ICallGateProvider GetIpcProvider(string name) => new CallGatePubSub(name); - /// - /// Gets an IPC subscriber. - /// - /// The return type for funcs. Use object if this is unused. - /// The name of the IPC registration. - /// An IPC subscriber. + /// public ICallGateSubscriber GetIpcSubscriber(string name) => new CallGatePubSub(name); - /// + /// public ICallGateSubscriber GetIpcSubscriber(string name) => new CallGatePubSub(name); - /// + /// public ICallGateSubscriber GetIpcSubscriber(string name) => new CallGatePubSub(name); - /// + /// public ICallGateSubscriber GetIpcSubscriber(string name) => new CallGatePubSub(name); - /// + /// public ICallGateSubscriber GetIpcSubscriber(string name) => new CallGatePubSub(name); - /// + /// public ICallGateSubscriber GetIpcSubscriber(string name) => new CallGatePubSub(name); - /// + /// public ICallGateSubscriber GetIpcSubscriber(string name) => new CallGatePubSub(name); - /// + /// public ICallGateSubscriber GetIpcSubscriber(string name) => new CallGatePubSub(name); - /// + /// public ICallGateSubscriber GetIpcSubscriber(string name) => new CallGatePubSub(name); @@ -399,10 +322,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa #region Configuration - /// - /// Save a plugin configuration(inheriting IPluginConfiguration). - /// - /// The current configuration. + /// public void SavePluginConfig(IPluginConfiguration? currentConfig) { if (currentConfig == null) @@ -411,10 +331,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa this.configs.Save(currentConfig, this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId); } - /// - /// Get a previously saved plugin configuration or null if none was saved before. - /// - /// A previously saved config or null if none was saved before. + /// public IPluginConfiguration? GetPluginConfig() { // This is done to support json deserialization of plugin configurations @@ -438,16 +355,10 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa return this.configs.Load(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId); } - /// - /// Get the config directory. - /// - /// directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName. + /// public string GetPluginConfigDirectory() => this.configs.GetDirectory(this.plugin.InternalName); - /// - /// Get the loc directory. - /// - /// directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName/loc. + /// public string GetPluginLocDirectory() => this.configs.GetDirectory(Path.Combine(this.plugin.InternalName, "loc")); #endregion @@ -508,8 +419,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa #endregion - /// Unregister the plugin and dispose all references. - /// Dalamud internal use only. + /// public void Dispose() { Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); From 9001c969864f16bddd0a302ee4e39d9378585f2d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 19 Oct 2025 23:30:25 +0200 Subject: [PATCH 179/477] Return the resulting service, not the Task --- Dalamud/IoC/Internal/ServiceContainer.cs | 2 +- Dalamud/IoC/Internal/ServiceScope.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 31d16e02e..6083f5a73 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -161,7 +161,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType public IServiceScope GetScope() => new ServiceScopeImpl(this); /// - object? IServiceProvider.GetService(Type serviceType) => this.GetSingletonService(serviceType); + public object? GetService(Type serviceType) => this.GetSingletonService(serviceType).Result; private async Task GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects) { diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 8b12dce0b..70d409b58 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -58,9 +58,9 @@ internal class ServiceScopeImpl : IServiceScope public ServiceScopeImpl(ServiceContainer container) => this.container = container; /// - object? IServiceProvider.GetService(Type serviceType) + public object? GetService(Type serviceType) { - return ((IServiceProvider)this.container).GetService(serviceType); + return this.container.GetService(serviceType); } /// From 9ea417c9ef75b94f3b2dbb9ffb8c905cfd9ec29e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 20 Oct 2025 10:45:02 +0200 Subject: [PATCH 180/477] Fix GetService getting stuck in Task --- Dalamud/IoC/Internal/ServiceContainer.cs | 21 ++++++++++++++++----- Dalamud/IoC/Internal/ServiceScope.cs | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 6083f5a73..6383b6b11 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -18,7 +18,7 @@ namespace Dalamud.IoC.Internal; /// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// [ServiceManager.ProvidedService] -internal class ServiceContainer : IServiceProvider, IServiceType +internal class ServiceContainer : IServiceType { private static readonly ModuleLog Log = new("SERVICECONTAINER"); @@ -160,10 +160,21 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// An implementation of a service scope. public IServiceScope GetScope() => new ServiceScopeImpl(this); - /// - public object? GetService(Type serviceType) => this.GetSingletonService(serviceType).Result; - - private async Task GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects) + /// + /// Resolves and returns an instance of the specified service type, using either singleton or scoped lifetime as + /// appropriate. + /// + /// The type of the service to resolve. This must be a concrete or interface type registered with the service + /// manager. + /// The scope within which to create scoped services. Required if the requested service type is registered as + /// scoped; otherwise, can be null. + /// An array of objects available for scoped resolution. Used to locate or create scoped service instances when + /// applicable. + /// An instance of the requested service type. Returns a singleton instance if available, a scoped instance if + /// required, or an object from the provided scoped objects if it matches the service type. + /// Thrown if a scoped service is requested but no scope is provided, or if the requested service type cannot be + /// resolved from the scoped objects. + public async Task GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects) { if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 70d409b58..c0c4e0b08 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -60,7 +60,7 @@ internal class ServiceScopeImpl : IServiceScope /// public object? GetService(Type serviceType) { - return this.container.GetService(serviceType); + return this.container.GetService(serviceType, this, []).ConfigureAwait(false).GetAwaiter().GetResult(); } /// From 53a082e68dad0e97d96a6239f2386548c06f3a9d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 20 Oct 2025 12:10:59 +0200 Subject: [PATCH 181/477] Check that PluginInterfaces implement IDalamudService --- Dalamud/Service/ServiceManager.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 9847f7147..88c6366fd 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -9,11 +9,14 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; +using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; using Dalamud.Storage; using Dalamud.Utility; using Dalamud.Utility.Timing; + using JetBrains.Annotations; // API10 TODO: Move to Dalamud.Service namespace. Some plugins reflect this... including my own, oops. There's a todo @@ -541,9 +544,11 @@ internal static class ServiceManager if (attr == null) return ServiceKind.None; - Debug.Assert( - type.IsAssignableTo(typeof(IServiceType)), - "Service did not inherit from IServiceType"); + if (!type.IsAssignableTo(typeof(IServiceType))) + { + Log.Error($"Service {type.Name} did not inherit from IServiceType"); + Debug.Fail("Service did not inherit from IServiceType"); + } if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedServiceAttribute))) return ServiceKind.BlockingEarlyLoadedService; @@ -552,7 +557,16 @@ internal static class ServiceManager return ServiceKind.EarlyLoadedService; if (attr.IsAssignableTo(typeof(ScopedServiceAttribute))) + { + if (type.GetCustomAttribute() != null + && !type.IsAssignableTo(typeof(IDalamudService))) + { + Log.Error($"Plugin-scoped service {type.Name} must inherit from IDalamudService"); + Debug.Fail("Plugin-scoped service must inherit from IDalamudService"); + } + return ServiceKind.ScopedService; + } return ServiceKind.ProvidedService; } From f4c9c16c68a0664966e9624c34ca7ede41d2205b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 17 Nov 2025 19:47:36 +0100 Subject: [PATCH 182/477] Mark IUnlockState with IDalamudService --- Dalamud/Plugin/Services/IUnlockState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 00f2df190..a0d733f55 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -11,7 +11,7 @@ namespace Dalamud.Plugin.Services; /// Interface for determining unlock state of various content in the game. /// [Experimental("UnlockState")] -public interface IUnlockState +public interface IUnlockState : IDalamudService { /// /// A delegate type used for the event. From 1bdad092ca1ecb1e4d5640ab8daace0f6fcd1f5f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 17 Nov 2025 20:28:52 +0100 Subject: [PATCH 183/477] Mark IPlayerState with IDalamudService --- Dalamud/Plugin/Services/IPlayerState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index 425ffc963..1416dfb77 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -12,7 +12,7 @@ namespace Dalamud.Plugin.Services; /// /// Interface for determining the players state. /// -public interface IPlayerState +public interface IPlayerState : IDalamudService { /// /// Gets a value indicating whether the local players data is loaded. From cb441631e1c51941b5b0d6a1e5f27c37ad4879ca Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 17 Nov 2025 20:58:14 +0100 Subject: [PATCH 184/477] Add empty ServiceConstructor to PlayerState --- Dalamud/Game/Player/PlayerState.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Game/Player/PlayerState.cs b/Dalamud/Game/Player/PlayerState.cs index 316b09e2f..917c946db 100644 --- a/Dalamud/Game/Player/PlayerState.cs +++ b/Dalamud/Game/Player/PlayerState.cs @@ -23,6 +23,11 @@ namespace Dalamud.Game.Player; [ResolveVia] internal unsafe class PlayerState : IServiceType, IPlayerState { + [ServiceManager.ServiceConstructor] + private PlayerState() + { + } + /// public bool IsLoaded => CSPlayerState.Instance()->IsLoaded; From 20041be27c3311b5ae0b2596a574c295e63ec1be Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 17 Nov 2025 23:55:55 +0100 Subject: [PATCH 185/477] Convert ReliableFileStorage to async --- .../Storage/ReliableFileStorageTests.cs | 94 +++++++++---------- .../Internal/DalamudConfiguration.cs | 20 ++-- Dalamud/Configuration/PluginConfigurations.cs | 9 +- Dalamud/EntryPoint.cs | 3 +- .../Windows/Data/Widgets/VfsWidget.cs | 4 +- Dalamud/Storage/ReliableFileStorage.cs | 49 +++++----- 6 files changed, 94 insertions(+), 85 deletions(-) diff --git a/Dalamud.Test/Storage/ReliableFileStorageTests.cs b/Dalamud.Test/Storage/ReliableFileStorageTests.cs index ff56293e0..8a955e430 100644 --- a/Dalamud.Test/Storage/ReliableFileStorageTests.cs +++ b/Dalamud.Test/Storage/ReliableFileStorageTests.cs @@ -31,19 +31,19 @@ public class ReliableFileStorageTests .Select( i => Parallel.ForEachAsync( Enumerable.Range(1, 100), - (j, _) => + async (j, _) => { if (i % 2 == 0) { // ReSharper disable once AccessToDisposedClosure - rfs.Instance.WriteAllText(tempFile, j.ToString()); + await rfs.Instance.WriteAllTextAsync(tempFile, j.ToString()); } else if (i % 3 == 0) { try { // ReSharper disable once AccessToDisposedClosure - rfs.Instance.ReadAllText(tempFile); + await rfs.Instance.ReadAllTextAsync(tempFile); } catch (FileNotFoundException) { @@ -54,8 +54,6 @@ public class ReliableFileStorageTests { File.Delete(tempFile); } - - return ValueTask.CompletedTask; }))); } @@ -112,41 +110,41 @@ public class ReliableFileStorageTests } [Fact] - public void Exists_WhenFileInBackup_ReturnsTrue() + public async Task Exists_WhenFileInBackup_ReturnsTrue() { var tempFile = Path.Combine(CreateTempDir(), TestFileName); using var rfs = CreateRfs(); - rfs.Instance.WriteAllText(tempFile, TestFileContent1); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1); File.Delete(tempFile); Assert.True(rfs.Instance.Exists(tempFile)); } [Fact] - public void Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse() + public async Task Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse() { var tempFile = Path.Combine(CreateTempDir(), TestFileName); using var rfs = CreateRfs(); - rfs.Instance.WriteAllText(tempFile, TestFileContent1); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1); File.Delete(tempFile); Assert.False(rfs.Instance.Exists(tempFile, Guid.NewGuid())); } [Fact] - public void WriteAllText_ThrowsIfPathIsEmpty() + public async Task WriteAllText_ThrowsIfPathIsEmpty() { using var rfs = CreateRfs(); - Assert.Throws(() => rfs.Instance.WriteAllText("", TestFileContent1)); + await Assert.ThrowsAsync(async () => await rfs.Instance.WriteAllTextAsync("", TestFileContent1)); } [Fact] - public void WriteAllText_ThrowsIfPathIsNull() + public async Task WriteAllText_ThrowsIfPathIsNull() { using var rfs = CreateRfs(); - Assert.Throws(() => rfs.Instance.WriteAllText(null!, TestFileContent1)); + await Assert.ThrowsAsync(async () => await rfs.Instance.WriteAllTextAsync(null!, TestFileContent1)); } [Fact] @@ -155,26 +153,26 @@ public class ReliableFileStorageTests var tempFile = Path.Combine(CreateTempDir(), TestFileName); using var rfs = CreateRfs(); - rfs.Instance.WriteAllText(tempFile, TestFileContent1); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1); Assert.True(File.Exists(tempFile)); - Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true)); + Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true)); Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile)); } [Fact] - public void WriteAllText_SeparatesContainers() + public async Task WriteAllText_SeparatesContainers() { var tempFile = Path.Combine(CreateTempDir(), TestFileName); var containerId = Guid.NewGuid(); using var rfs = CreateRfs(); - rfs.Instance.WriteAllText(tempFile, TestFileContent1); - rfs.Instance.WriteAllText(tempFile, TestFileContent2, containerId); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent2, containerId); File.Delete(tempFile); - Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true)); - Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true, containerId)); + Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true)); + Assert.Equal(TestFileContent2, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true, containerId)); } [Fact] @@ -183,7 +181,7 @@ public class ReliableFileStorageTests var tempFile = Path.Combine(CreateTempDir(), TestFileName); using var rfs = CreateFailedRfs(); - rfs.Instance.WriteAllText(tempFile, TestFileContent1); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1); Assert.True(File.Exists(tempFile)); Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile)); @@ -195,38 +193,38 @@ public class ReliableFileStorageTests var tempFile = Path.Combine(CreateTempDir(), TestFileName); using var rfs = CreateRfs(); - rfs.Instance.WriteAllText(tempFile, TestFileContent1); - rfs.Instance.WriteAllText(tempFile, TestFileContent2); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent2); Assert.True(File.Exists(tempFile)); - Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true)); + Assert.Equal(TestFileContent2, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true)); Assert.Equal(TestFileContent2, await File.ReadAllTextAsync(tempFile)); } [Fact] - public void WriteAllText_SupportsNullContent() + public async Task WriteAllText_SupportsNullContent() { var tempFile = Path.Combine(CreateTempDir(), TestFileName); using var rfs = CreateRfs(); - rfs.Instance.WriteAllText(tempFile, null); + await rfs.Instance.WriteAllTextAsync(tempFile, null); Assert.True(File.Exists(tempFile)); - Assert.Equal("", rfs.Instance.ReadAllText(tempFile)); + Assert.Equal("", await rfs.Instance.ReadAllTextAsync(tempFile)); } [Fact] - public void ReadAllText_ThrowsIfPathIsEmpty() + public async Task ReadAllText_ThrowsIfPathIsEmpty() { using var rfs = CreateRfs(); - Assert.Throws(() => rfs.Instance.ReadAllText("")); + await Assert.ThrowsAsync(async () => await rfs.Instance.ReadAllTextAsync("")); } [Fact] - public void ReadAllText_ThrowsIfPathIsNull() + public async Task ReadAllText_ThrowsIfPathIsNull() { using var rfs = CreateRfs(); - Assert.Throws(() => rfs.Instance.ReadAllText(null!)); + await Assert.ThrowsAsync(async () => await rfs.Instance.ReadAllTextAsync(null!)); } [Fact] @@ -236,40 +234,40 @@ public class ReliableFileStorageTests await File.WriteAllTextAsync(tempFile, TestFileContent1); using var rfs = CreateRfs(); - Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile)); + Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile)); } [Fact] - public void ReadAllText_WhenFileMissingWithBackup_ReturnsContent() + public async Task ReadAllText_WhenFileMissingWithBackup_ReturnsContent() { var tempFile = Path.Combine(CreateTempDir(), TestFileName); using var rfs = CreateRfs(); - rfs.Instance.WriteAllText(tempFile, TestFileContent1); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1); File.Delete(tempFile); - Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile)); + Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile)); } [Fact] - public void ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId() + public async Task ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId() { var tempFile = Path.Combine(CreateTempDir(), TestFileName); var containerId = Guid.NewGuid(); using var rfs = CreateRfs(); - rfs.Instance.WriteAllText(tempFile, TestFileContent1); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1); File.Delete(tempFile); - Assert.Throws(() => rfs.Instance.ReadAllText(tempFile, containerId: containerId)); + await Assert.ThrowsAsync(async () => await rfs.Instance.ReadAllTextAsync(tempFile, containerId: containerId)); } [Fact] - public void ReadAllText_WhenFileMissing_ThrowsIfDbFailed() + public async Task ReadAllText_WhenFileMissing_ThrowsIfDbFailed() { var tempFile = Path.Combine(CreateTempDir(), TestFileName); using var rfs = CreateFailedRfs(); - Assert.Throws(() => rfs.Instance.ReadAllText(tempFile)); + await Assert.ThrowsAsync(async () => await rfs.Instance.ReadAllTextAsync(tempFile)); } [Fact] @@ -278,7 +276,7 @@ public class ReliableFileStorageTests var tempFile = Path.Combine(CreateTempDir(), TestFileName); await File.WriteAllTextAsync(tempFile, TestFileContent1); using var rfs = CreateRfs(); - rfs.Instance.ReadAllText(tempFile, text => Assert.Equal(TestFileContent1, text)); + await rfs.Instance.ReadAllTextAsync(tempFile, text => Assert.Equal(TestFileContent1, text)); } [Fact] @@ -290,7 +288,7 @@ public class ReliableFileStorageTests var readerCalledOnce = false; using var rfs = CreateRfs(); - Assert.Throws(() => rfs.Instance.ReadAllText(tempFile, Reader)); + await Assert.ThrowsAsync(async () => await rfs.Instance.ReadAllTextAsync(tempFile, Reader)); return; @@ -303,7 +301,7 @@ public class ReliableFileStorageTests } [Fact] - public void ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup() + public async Task ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup() { var tempFile = Path.Combine(CreateTempDir(), TestFileName); @@ -311,10 +309,10 @@ public class ReliableFileStorageTests var assertionCalled = false; using var rfs = CreateRfs(); - rfs.Instance.WriteAllText(tempFile, TestFileContent1); + await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1); File.Delete(tempFile); - rfs.Instance.ReadAllText(tempFile, Reader); + await rfs.Instance.ReadAllTextAsync(tempFile, Reader); Assert.True(assertionCalled); return; @@ -335,17 +333,17 @@ public class ReliableFileStorageTests var tempFile = Path.Combine(CreateTempDir(), TestFileName); await File.WriteAllTextAsync(tempFile, TestFileContent1); using var rfs = CreateRfs(); - Assert.Throws(() => rfs.Instance.ReadAllText(tempFile, _ => throw new FileNotFoundException())); + await Assert.ThrowsAsync(async () => await rfs.Instance.ReadAllTextAsync(tempFile, _ => throw new FileNotFoundException())); } [Theory] [InlineData(true)] [InlineData(false)] - public void ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup) + public async Task ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup) { var tempFile = Path.Combine(CreateTempDir(), TestFileName); using var rfs = CreateRfs(); - Assert.Throws(() => rfs.Instance.ReadAllText(tempFile, forceBackup)); + await Assert.ThrowsAsync(async () => await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup)); } private static DisposableReliableFileStorage CreateRfs() diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 241a08d90..da0d7c2c6 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -503,13 +503,13 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// Path to read from. /// File storage. /// The deserialized configuration file. - public static DalamudConfiguration Load(string path, ReliableFileStorage fs) + public static async Task Load(string path, ReliableFileStorage fs) { DalamudConfiguration deserialized = null; try { - fs.ReadAllText(path, text => + await fs.ReadAllTextAsync(path, text => { deserialized = JsonConvert.DeserializeObject(text, SerializerSettings); @@ -580,8 +580,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService { this.Save(); this.isSaveQueued = false; - - Log.Verbose("Config saved"); } } @@ -630,16 +628,20 @@ internal sealed class DalamudConfiguration : IInternalDisposableService // Wait for previous write to finish this.writeTask?.Wait(); - this.writeTask = Task.Run(() => + this.writeTask = Task.Run(async () => { - Service.Get().WriteAllText( - this.configPath, - JsonConvert.SerializeObject(this, SerializerSettings)); + await Service.Get().WriteAllTextAsync( + this.configPath, + JsonConvert.SerializeObject(this, SerializerSettings)); + Log.Verbose("DalamudConfiguration saved"); }).ContinueWith(t => { if (t.IsFaulted) { - Log.Error(t.Exception, "Failed to save DalamudConfiguration to {Path}", this.configPath); + Log.Error( + t.Exception, + "Failed to save DalamudConfiguration to {Path}", + this.configPath); } }); diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index 8e32fa992..fa2969d31 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -2,6 +2,8 @@ using System.IO; using System.Reflection; using Dalamud.Storage; +using Dalamud.Utility; + using Newtonsoft.Json; namespace Dalamud.Configuration; @@ -9,6 +11,7 @@ namespace Dalamud.Configuration; /// /// Configuration to store settings for a dalamud plugin. /// +[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; @@ -36,7 +39,7 @@ public sealed class PluginConfigurations public void Save(IPluginConfiguration config, string pluginName, Guid workingPluginId) { Service.Get() - .WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId); + .WriteAllTextAsync(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId).GetAwaiter().GetResult(); } /// @@ -52,12 +55,12 @@ public sealed class PluginConfigurations IPluginConfiguration? config = null; try { - Service.Get().ReadAllText(path.FullName, text => + Service.Get().ReadAllTextAsync(path.FullName, text => { config = DeserializeConfig(text); if (config == null) throw new Exception("Read config was null."); - }, workingPluginId); + }, workingPluginId).GetAwaiter().GetResult(); } catch (FileNotFoundException) { diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 9fc09a56b..15077f3d8 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -144,7 +144,8 @@ public sealed class EntryPoint // Load configuration first to get some early persistent state, like log level var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!); - var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs); + var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs) + .GetAwaiter().GetResult(); // Set the appropriate logging level from the configuration if (!configuration.LogSynchronously) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs index f1f2476c9..f044b2989 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs @@ -52,7 +52,7 @@ internal class VfsWidget : IDataWindowWidget for (var i = 0; i < this.reps; i++) { stopwatch.Restart(); - service.WriteAllBytes(path, data); + service.WriteAllBytesAsync(path, data).GetAwaiter().GetResult(); stopwatch.Stop(); acc += stopwatch.ElapsedMilliseconds; Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds); @@ -70,7 +70,7 @@ internal class VfsWidget : IDataWindowWidget for (var i = 0; i < this.reps; i++) { stopwatch.Restart(); - service.ReadAllBytes(path); + service.ReadAllBytesAsync(path).GetAwaiter().GetResult(); stopwatch.Stop(); acc += stopwatch.ElapsedMilliseconds; Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds); diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 9791b9e45..d9f8526c3 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -1,5 +1,6 @@ using System.IO; using System.Text; +using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Utility; @@ -92,8 +93,9 @@ internal class ReliableFileStorage : IInternalDisposableService /// Path to write to. /// The contents of the file. /// Container to write to. - public void WriteAllText(string path, string? contents, Guid containerId = default) - => this.WriteAllText(path, contents, Encoding.UTF8, containerId); + /// A representing the asynchronous operation. + public async Task WriteAllTextAsync(string path, string? contents, Guid containerId = default) + => await this.WriteAllTextAsync(path, contents, Encoding.UTF8, containerId); /// /// Write all text to a file. @@ -102,10 +104,11 @@ internal class ReliableFileStorage : IInternalDisposableService /// The contents of the file. /// The encoding to write with. /// Container to write to. - public void WriteAllText(string path, string? contents, Encoding encoding, Guid containerId = default) + /// A representing the asynchronous operation. + public async Task WriteAllTextAsync(string path, string? contents, Encoding encoding, Guid containerId = default) { var bytes = encoding.GetBytes(contents ?? string.Empty); - this.WriteAllBytes(path, bytes, containerId); + await this.WriteAllBytesAsync(path, bytes, containerId); } /// @@ -114,7 +117,8 @@ internal class ReliableFileStorage : IInternalDisposableService /// Path to write to. /// The contents of the file. /// Container to write to. - public void WriteAllBytes(string path, byte[] bytes, Guid containerId = default) + /// A representing the asynchronous operation. + public Task WriteAllBytesAsync(string path, byte[] bytes, Guid containerId = default) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -123,7 +127,7 @@ internal class ReliableFileStorage : IInternalDisposableService if (this.db == null) { FilesystemUtil.WriteAllBytesSafe(path, bytes); - return; + return Task.CompletedTask; } this.db.RunInTransaction(() => @@ -149,6 +153,8 @@ internal class ReliableFileStorage : IInternalDisposableService FilesystemUtil.WriteAllBytesSafe(path, bytes); }); } + + return Task.CompletedTask; } /// @@ -161,8 +167,8 @@ internal class ReliableFileStorage : IInternalDisposableService /// The container to read from. /// All text stored in this file. /// Thrown if the file does not exist on the filesystem or in the backup. - public string ReadAllText(string path, bool forceBackup = false, Guid containerId = default) - => this.ReadAllText(path, Encoding.UTF8, forceBackup, containerId); + public Task ReadAllTextAsync(string path, bool forceBackup = false, Guid containerId = default) + => this.ReadAllTextAsync(path, Encoding.UTF8, forceBackup, containerId); /// /// Read all text from a file. @@ -175,9 +181,9 @@ internal class ReliableFileStorage : IInternalDisposableService /// The container to read from. /// All text stored in this file. /// Thrown if the file does not exist on the filesystem or in the backup. - public string ReadAllText(string path, Encoding encoding, bool forceBackup = false, Guid containerId = default) + public async Task ReadAllTextAsync(string path, Encoding encoding, bool forceBackup = false, Guid containerId = default) { - var bytes = this.ReadAllBytes(path, forceBackup, containerId); + var bytes = await this.ReadAllBytesAsync(path, forceBackup, containerId); return encoding.GetString(bytes); } @@ -191,8 +197,9 @@ internal class ReliableFileStorage : IInternalDisposableService /// The container to read from. /// Thrown if the file does not exist on the filesystem or in the backup. /// Thrown here if the file and the backup fail their read. - public void ReadAllText(string path, Action reader, Guid containerId = default) - => this.ReadAllText(path, Encoding.UTF8, reader, containerId); + /// A representing the asynchronous operation. + public async Task ReadAllTextAsync(string path, Action reader, Guid containerId = default) + => await this.ReadAllTextAsync(path, Encoding.UTF8, reader, containerId); /// /// Read all text from a file, and automatically try again with the backup if the file does not exist or @@ -205,7 +212,8 @@ internal class ReliableFileStorage : IInternalDisposableService /// The container to read from. /// Thrown if the file does not exist on the filesystem or in the backup. /// Thrown here if the file and the backup fail their read. - public void ReadAllText(string path, Encoding encoding, Action reader, Guid containerId = default) + /// A representing the asynchronous operation. + public async Task ReadAllTextAsync(string path, Encoding encoding, Action reader, Guid containerId = default) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -216,7 +224,7 @@ internal class ReliableFileStorage : IInternalDisposableService // 1.) Try without using the backup try { - var text = this.ReadAllText(path, encoding, false, containerId); + var text = await this.ReadAllTextAsync(path, encoding, false, containerId); reader(text); return; } @@ -233,7 +241,7 @@ internal class ReliableFileStorage : IInternalDisposableService // 2.) Try using the backup try { - var text = this.ReadAllText(path, encoding, true, containerId); + var text = await this.ReadAllTextAsync(path, encoding, true, containerId); reader(text); } catch (Exception ex) @@ -253,7 +261,7 @@ internal class ReliableFileStorage : IInternalDisposableService /// The container to read from. /// All bytes stored in this file. /// Thrown if the file does not exist on the filesystem or in the backup. - public byte[] ReadAllBytes(string path, bool forceBackup = false, Guid containerId = default) + public async Task ReadAllBytesAsync(string path, bool forceBackup = false, Guid containerId = default) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -265,15 +273,12 @@ internal class ReliableFileStorage : IInternalDisposableService var normalizedPath = NormalizePath(path); var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); - if (file == null) - throw new FileNotFoundException(); - - return file.Data; + return file == null ? throw new FileNotFoundException() : file.Data; } // If the file doesn't exist, immediately check the backup db if (!File.Exists(path)) - return this.ReadAllBytes(path, true, containerId); + return await this.ReadAllBytesAsync(path, true, containerId); try { @@ -282,7 +287,7 @@ internal class ReliableFileStorage : IInternalDisposableService catch (Exception e) { Log.Error(e, "Failed to read file from disk, falling back to database"); - return this.ReadAllBytes(path, true, containerId); + return await this.ReadAllBytesAsync(path, true, containerId); } } From cc9191657453986cb6451f63f3238b3b29b7e26c Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 00:52:30 +0100 Subject: [PATCH 186/477] Fix bad merge --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 6 ++---- Dalamud/Game/ClientState/Party/PartyList.cs | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index a76b520af..4d5fc2aab 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -9,8 +9,6 @@ using Dalamud.Plugin.Services; using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; - -using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; namespace Dalamud.Game.ClientState.Buddy; @@ -74,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } } - private unsafe CSBuddy* BuddyListStruct => &UIState.Instance()->Buddy; + private unsafe CSBuddy* BuddyListStruct => &CSUIState.Instance()->Buddy; /// public IBuddyMember? this[int index] @@ -113,7 +111,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList if (address == 0) return null; - if (this.clientState.LocalContentId == 0) + if (this.playerState.ContentId == 0) return null; var buddy = new BuddyMember((CSBuddyMember*)address); diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 1a5177b10..1dede1dd3 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -93,7 +93,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreatePartyMemberReference(nint address) { - if (this.clientState.LocalContentId == 0) + if (this.playerState.ContentId == 0) return null; if (address == 0) @@ -114,7 +114,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreateAllianceMemberReference(nint address) { - if (this.clientState.LocalContentId == 0) + if (this.playerState.ContentId == 0) return null; if (address == 0) From 6a69a6e197ac6761d6c3d39fe3a879c852151cf6 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 00:58:08 +0100 Subject: [PATCH 187/477] Fix some warnings --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 2 +- Dalamud/GlobalSuppressions.cs | 1 + Dalamud/Networking/Pipes/DalamudUri.cs | 34 +++++++++---------- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 5 +-- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 4d5fc2aab..b8e4c0fcc 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -8,8 +8,8 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; -using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; +using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; namespace Dalamud.Game.ClientState.Buddy; diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs index 8a9d31b12..35754eb04 100644 --- a/Dalamud/GlobalSuppressions.cs +++ b/Dalamud/GlobalSuppressions.cs @@ -21,6 +21,7 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments", Justification = "I like having comments in blocks")] // ImRAII stuff [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Pipes/DalamudUri.cs index 03ad15af1..7e639cbbe 100644 --- a/Dalamud/Networking/Pipes/DalamudUri.cs +++ b/Dalamud/Networking/Pipes/DalamudUri.cs @@ -61,6 +61,23 @@ public record DalamudUri /// public override string ToString() => this.rawUri.ToString(); + /// + /// Build a DalamudURI from a given URI. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(Uri uri) + { + return new DalamudUri(uri); + } + + /// + /// Build a DalamudURI from a URI in string format. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); + private string[] GetDataSegments() { // reimplementation of the System.URI#Segments, under MIT license. @@ -82,21 +99,4 @@ public record DalamudUri return segments.ToArray(); } - - /// - /// Build a DalamudURI from a given URI. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(Uri uri) - { - return new DalamudUri(uri); - } - - /// - /// Build a DalamudURI from a URI in string format. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); } diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 22139814d..5d2d32728 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -6,7 +6,7 @@ namespace Dalamud.Plugin.Services; /// /// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the -/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. +/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// [Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] public interface IPluginLinkHandler @@ -14,7 +14,8 @@ public interface IPluginLinkHandler /// /// A delegate containing the received URI. /// - delegate void PluginUriReceived(DalamudUri uri); + /// The URI opened by the user. + public delegate void PluginUriReceived(DalamudUri uri); /// /// The event fired when a URI targeting this plugin is received. From 05648f019be5365f1c98479da67428e83192711f Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 20:37:57 +0100 Subject: [PATCH 188/477] First draft of IReliableFileStorage service --- .../Plugin/Services/IReliableFileStorage.cs | 163 ++++++++++++++++++ .../ReliableFileStoragePluginScoped.cs | 120 +++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 Dalamud/Plugin/Services/IReliableFileStorage.cs create mode 100644 Dalamud/Storage/ReliableFileStoragePluginScoped.cs diff --git a/Dalamud/Plugin/Services/IReliableFileStorage.cs b/Dalamud/Plugin/Services/IReliableFileStorage.cs new file mode 100644 index 000000000..3a3c070f0 --- /dev/null +++ b/Dalamud/Plugin/Services/IReliableFileStorage.cs @@ -0,0 +1,163 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; + +using Dalamud.Storage; + +namespace Dalamud.Plugin.Services; + +/// +/// Service to interact with the file system, as a replacement for standard C# file I/O. +/// Writes and reads using this service are, to the best of our ability, atomic and reliable. +/// +/// All data is synced to disk immediately and written to a database, additionally to files on disk. This means +/// that in case of file corruption, data can likely be recovered from the database. +/// +/// However, this also means that operations using this service duplicate data on disk, so we don't recommend +/// performing large file operations. The service will not permit files larger than +/// (64MB) to be written. +/// +public interface IReliableFileStorage : IDalamudService +{ + /// + /// Gets the maximum file size, in bytes, that can be written using this service. + /// + /// + /// The service enforces this limit when writing files and fails with an appropriate exception + /// (for example or a custom exception) when a caller attempts to write + /// more than this number of bytes. + /// + long MaxFileSizeBytes { get; } + + /// + /// Check whether a file exists either on the local filesystem or in the transparent backup database. + /// + /// The file system path to check. Must not be null or empty. + /// + /// True if the file exists on disk or a backup copy exists in the storage's internal journal/backup database; + /// otherwise false. + /// + /// Thrown when is null or empty. + bool Exists(string path); + + /// + /// Write the given text into a file using UTF-8 encoding. The write is performed atomically and is persisted to + /// both the filesystem and the internal backup database used by this service. + /// + /// The file path to write to. Must not be null or empty. + /// The string contents to write. May be null, in which case an empty file is written. + /// A that completes when the write has finished and been flushed to disk and the backup. + /// Thrown when is null or empty. + Task WriteAllTextAsync(string path, string? contents); + + /// + /// Write the given text into a file using the provided . The write is performed + /// atomically (to the extent possible) and is persisted to both the filesystem and the internal backup database + /// used by this service. + /// + /// The file path to write to. Must not be null or empty. + /// The string contents to write. May be null, in which case an empty file is written. + /// The text encoding to use when serializing the string to bytes. Must not be null. + /// A that completes when the write has finished and been flushed to disk and the backup. + /// Thrown when is null or empty. + /// Thrown when is null. + Task WriteAllTextAsync(string path, string? contents, Encoding encoding); + + /// + /// Write the given bytes to a file. The write is persisted to both the filesystem and the service's internal + /// backup database. Avoid writing extremely large byte arrays because this service duplicates data on disk. + /// + /// The file path to write to. Must not be null or empty. + /// The raw bytes to write. Must not be null. + /// A that completes when the write has finished and been flushed to disk and the backup. + /// Thrown when is null or empty. + /// Thrown when is null. + Task WriteAllBytesAsync(string path, byte[] bytes); + + /// + /// Read all text from a file using UTF-8 encoding. If the file is unreadable or missing on disk, the service + /// attempts to return a backed-up copy from its internal journal/backup database. + /// + /// The file path to read. Must not be null or empty. + /// + /// When true the service prefers the internal backup database and returns backed-up contents if available. When + /// false the service tries the filesystem first and falls back to the backup only on error or when the file is missing. + /// + /// The textual contents of the file, decoded using UTF-8. + /// Thrown when is null or empty. + /// Thrown when the file does not exist on disk and no backup copy is available. + Task ReadAllTextAsync(string path, bool forceBackup = false); + + /// + /// Read all text from a file using the specified . If the file is unreadable or + /// missing on disk, the service attempts to return a backed-up copy from its internal journal/backup database. + /// + /// The file path to read. Must not be null or empty. + /// The encoding to use when decoding the stored bytes into text. Must not be null. + /// + /// When true the service prefers the internal backup database and returns backed-up contents if available. When + /// false the service tries the filesystem first and falls back to the backup only on error or when the file is missing. + /// + /// The textual contents of the file decoded using the provided . + /// Thrown when is null or empty. + /// Thrown when is null. + /// Thrown when the file does not exist on disk and no backup copy is available. + Task ReadAllTextAsync(string path, Encoding encoding, bool forceBackup = false); + + /// + /// Read all text from a file and invoke the provided callback with the string + /// contents. If the reader throws or the initial read fails, the service attempts a backup read and invokes the + /// reader again with the backup contents. If both reads fail the service surfaces an exception to the caller. + /// + /// The file path to read. Must not be null or empty. + /// + /// A callback invoked with the file's textual contents. Must not be null. + /// If the callback throws an exception the service treats that as a signal to retry the read using the + /// internal backup database and will invoke the callback again with the backup contents when available. + /// For example, the callback can throw when JSON deserialization fails to request the backup copy instead of + /// silently accepting corrupt data. + /// + /// A that completes when the read (and any attempted fallback) and callback invocation have finished. + /// Thrown when is null or empty. + /// Thrown when is null. + /// Thrown when the file does not exist on disk and no backup copy is available. + /// Thrown when both the filesystem read and the backup read fail for other reasons. + Task ReadAllTextAsync(string path, Action reader); + + /// + /// Read all text from a file using the specified and invoke the provided + /// callback with the decoded string contents. If the reader throws or the initial + /// read fails, the service attempts a backup read and invokes the reader again with the backup contents. If + /// both reads fail the service surfaces an exception to the caller. + /// + /// The file path to read. Must not be null or empty. + /// The encoding to use when decoding the stored bytes into text. Must not be null. + /// + /// A callback invoked with the file's textual contents. Must not be null. + /// If the callback throws an exception the service treats that as a signal to retry the read using the + /// internal backup database and will invoke the callback again with the backup contents when available. + /// For example, the callback can throw when JSON deserialization fails to request the backup copy instead of + /// silently accepting corrupt data. + /// + /// A that completes when the read (and any attempted fallback) and callback invocation have finished. + /// Thrown when is null or empty. + /// Thrown when or is null. + /// Thrown when the file does not exist on disk and no backup copy is available. + /// Thrown when both the filesystem read and the backup read fail for other reasons. + Task ReadAllTextAsync(string path, Encoding encoding, Action reader); + + /// + /// Read all bytes from a file. If the file is unreadable or missing on disk, the service may try to return a + /// backed-up copy from its internal journal/backup database. + /// + /// The file path to read. Must not be null or empty. + /// + /// When true the service prefers the internal backup database and returns the backed-up contents + /// if available. When false the service tries the filesystem first and falls back to the backup only + /// on error or when the file is missing. + /// + /// The raw bytes stored in the file. + /// Thrown when is null or empty. + /// Thrown when the file does not exist on disk and no backup copy is available. + Task ReadAllBytesAsync(string path, bool forceBackup = false); +} diff --git a/Dalamud/Storage/ReliableFileStoragePluginScoped.cs b/Dalamud/Storage/ReliableFileStoragePluginScoped.cs new file mode 100644 index 000000000..1f1992da6 --- /dev/null +++ b/Dalamud/Storage/ReliableFileStoragePluginScoped.cs @@ -0,0 +1,120 @@ +using System.Threading.Tasks; +using System.Text; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +namespace Dalamud.Storage; + +[PluginInterface] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public class ReliableFileStoragePluginScoped : IReliableFileStorage, IServiceType +{ + // TODO: Make sure pending writes are finalized on plugin unload? + + private readonly LocalPlugin plugin; + + [ServiceManager.ServiceDependency] + private readonly ReliableFileStorage storage = Service.Get(); + + [ServiceManager.ServiceConstructor] + internal ReliableFileStoragePluginScoped(LocalPlugin plugin) + { + this.plugin = plugin; + } + + /// + public long MaxFileSizeBytes => 64 * 1024 * 1024; + + /// + public bool Exists(string path) + { + return this.storage.Exists(path, this.plugin.EffectiveWorkingPluginId); + } + + /// + public Task WriteAllTextAsync(string path, string? contents) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + var bytes = Encoding.UTF8.GetBytes(contents ?? string.Empty); + if (bytes.LongLength > this.MaxFileSizeBytes) + throw new ArgumentException($"The provided data exceeds the maximum allowed size of {this.MaxFileSizeBytes} bytes.", nameof(contents)); + + return this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId); + } + + /// + public Task WriteAllTextAsync(string path, string? contents, Encoding encoding) + { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentNullException.ThrowIfNull(encoding); + + var bytes = encoding.GetBytes(contents ?? string.Empty); + if (bytes.LongLength > this.MaxFileSizeBytes) + throw new ArgumentException($"The provided data exceeds the maximum allowed size of {this.MaxFileSizeBytes} bytes.", nameof(contents)); + + return this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId); + } + + /// + public Task WriteAllBytesAsync(string path, byte[] bytes) + { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentNullException.ThrowIfNull(bytes); + + if (bytes.LongLength > this.MaxFileSizeBytes) + throw new ArgumentException($"The provided data exceeds the maximum allowed size of {this.MaxFileSizeBytes} bytes.", nameof(bytes)); + + return this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId); + } + + /// + public Task ReadAllTextAsync(string path, bool forceBackup = false) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + return this.storage.ReadAllTextAsync(path, forceBackup, this.plugin.EffectiveWorkingPluginId); + } + + /// + public Task ReadAllTextAsync(string path, Encoding encoding, bool forceBackup = false) + { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentNullException.ThrowIfNull(encoding); + + return this.storage.ReadAllTextAsync(path, encoding, forceBackup, this.plugin.EffectiveWorkingPluginId); + } + + /// + public Task ReadAllTextAsync(string path, Action reader) + { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentNullException.ThrowIfNull(reader); + + return this.storage.ReadAllTextAsync(path, reader, this.plugin.EffectiveWorkingPluginId); + } + + /// + public Task ReadAllTextAsync(string path, Encoding encoding, Action reader) + { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentNullException.ThrowIfNull(encoding); + ArgumentNullException.ThrowIfNull(reader); + + return this.storage.ReadAllTextAsync(path, encoding, reader, this.plugin.EffectiveWorkingPluginId); + } + + /// + public Task ReadAllBytesAsync(string path, bool forceBackup = false) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + return this.storage.ReadAllBytesAsync(path, forceBackup, this.plugin.EffectiveWorkingPluginId); + } +} From f831a7c010240661969062e457ca4d59379b0a99 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 21:39:47 +0100 Subject: [PATCH 189/477] Wait for pending writes when disposing service --- .../Plugin/Services/IReliableFileStorage.cs | 3 + .../ReliableFileStoragePluginScoped.cs | 101 +++++++++++++++--- 2 files changed, 92 insertions(+), 12 deletions(-) diff --git a/Dalamud/Plugin/Services/IReliableFileStorage.cs b/Dalamud/Plugin/Services/IReliableFileStorage.cs index 3a3c070f0..93c2df243 100644 --- a/Dalamud/Plugin/Services/IReliableFileStorage.cs +++ b/Dalamud/Plugin/Services/IReliableFileStorage.cs @@ -2,6 +2,7 @@ using System.IO; using System.Text; using System.Threading.Tasks; +using Dalamud.Configuration; using Dalamud.Storage; namespace Dalamud.Plugin.Services; @@ -16,6 +17,8 @@ namespace Dalamud.Plugin.Services; /// However, this also means that operations using this service duplicate data on disk, so we don't recommend /// performing large file operations. The service will not permit files larger than /// (64MB) to be written. +/// +/// Saved configuration data using the class uses this functionality implicitly. /// public interface IReliableFileStorage : IDalamudService { diff --git a/Dalamud/Storage/ReliableFileStoragePluginScoped.cs b/Dalamud/Storage/ReliableFileStoragePluginScoped.cs index 1f1992da6..f6598a087 100644 --- a/Dalamud/Storage/ReliableFileStoragePluginScoped.cs +++ b/Dalamud/Storage/ReliableFileStoragePluginScoped.cs @@ -1,5 +1,8 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using Dalamud.IoC; using Dalamud.IoC.Internal; @@ -8,20 +11,31 @@ using Dalamud.Plugin.Services; namespace Dalamud.Storage; +/// +/// Plugin-scoped VFS wrapper. +/// [PluginInterface] [ServiceManager.ScopedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class ReliableFileStoragePluginScoped : IReliableFileStorage, IServiceType +public class ReliableFileStoragePluginScoped : IReliableFileStorage, IInternalDisposableService { - // TODO: Make sure pending writes are finalized on plugin unload? + private readonly Lock pendingLock = new(); + private readonly HashSet pendingWrites = []; private readonly LocalPlugin plugin; [ServiceManager.ServiceDependency] private readonly ReliableFileStorage storage = Service.Get(); + // When true, the scope is disposing and new write requests are rejected. + private volatile bool isDisposing = false; + + /// + /// Initializes a new instance of the class. + /// + /// The owner plugin. [ServiceManager.ServiceConstructor] internal ReliableFileStoragePluginScoped(LocalPlugin plugin) { @@ -34,32 +48,31 @@ public class ReliableFileStoragePluginScoped : IReliableFileStorage, IServiceTyp /// public bool Exists(string path) { + if (this.isDisposing) + throw new ObjectDisposedException(nameof(ReliableFileStoragePluginScoped)); + return this.storage.Exists(path, this.plugin.EffectiveWorkingPluginId); } /// public Task WriteAllTextAsync(string path, string? contents) { + // Route through WriteAllBytesAsync so all write tracking and size checks are centralized. ArgumentException.ThrowIfNullOrEmpty(path); var bytes = Encoding.UTF8.GetBytes(contents ?? string.Empty); - if (bytes.LongLength > this.MaxFileSizeBytes) - throw new ArgumentException($"The provided data exceeds the maximum allowed size of {this.MaxFileSizeBytes} bytes.", nameof(contents)); - - return this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId); + return this.WriteAllBytesAsync(path, bytes); } /// public Task WriteAllTextAsync(string path, string? contents, Encoding encoding) { + // Route through WriteAllBytesAsync so all write tracking and size checks are centralized. ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(encoding); var bytes = encoding.GetBytes(contents ?? string.Empty); - if (bytes.LongLength > this.MaxFileSizeBytes) - throw new ArgumentException($"The provided data exceeds the maximum allowed size of {this.MaxFileSizeBytes} bytes.", nameof(contents)); - - return this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId); + return this.WriteAllBytesAsync(path, bytes); } /// @@ -71,12 +84,36 @@ public class ReliableFileStoragePluginScoped : IReliableFileStorage, IServiceTyp if (bytes.LongLength > this.MaxFileSizeBytes) throw new ArgumentException($"The provided data exceeds the maximum allowed size of {this.MaxFileSizeBytes} bytes.", nameof(bytes)); - return this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId); + // Start the underlying write task + var task = Task.Run(() => this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId)); + + // Track the task so we can wait for it on dispose + lock (this.pendingLock) + { + if (this.isDisposing) + throw new ObjectDisposedException(nameof(ReliableFileStoragePluginScoped)); + + this.pendingWrites.Add(task); + } + + // Remove when done, if the task is already done this runs synchronously here and removes immediately + _ = task.ContinueWith(t => + { + lock (this.pendingLock) + { + this.pendingWrites.Remove(t); + } + }, TaskContinuationOptions.ExecuteSynchronously); + + return task; } /// public Task ReadAllTextAsync(string path, bool forceBackup = false) { + if (this.isDisposing) + throw new ObjectDisposedException(nameof(ReliableFileStoragePluginScoped)); + ArgumentException.ThrowIfNullOrEmpty(path); return this.storage.ReadAllTextAsync(path, forceBackup, this.plugin.EffectiveWorkingPluginId); @@ -85,6 +122,9 @@ public class ReliableFileStoragePluginScoped : IReliableFileStorage, IServiceTyp /// public Task ReadAllTextAsync(string path, Encoding encoding, bool forceBackup = false) { + if (this.isDisposing) + throw new ObjectDisposedException(nameof(ReliableFileStoragePluginScoped)); + ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(encoding); @@ -94,6 +134,9 @@ public class ReliableFileStoragePluginScoped : IReliableFileStorage, IServiceTyp /// public Task ReadAllTextAsync(string path, Action reader) { + if (this.isDisposing) + throw new ObjectDisposedException(nameof(ReliableFileStoragePluginScoped)); + ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(reader); @@ -103,6 +146,9 @@ public class ReliableFileStoragePluginScoped : IReliableFileStorage, IServiceTyp /// public Task ReadAllTextAsync(string path, Encoding encoding, Action reader) { + if (this.isDisposing) + throw new ObjectDisposedException(nameof(ReliableFileStoragePluginScoped)); + ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(encoding); ArgumentNullException.ThrowIfNull(reader); @@ -113,8 +159,39 @@ public class ReliableFileStoragePluginScoped : IReliableFileStorage, IServiceTyp /// public Task ReadAllBytesAsync(string path, bool forceBackup = false) { + if (this.isDisposing) + throw new ObjectDisposedException(nameof(ReliableFileStoragePluginScoped)); + ArgumentException.ThrowIfNullOrEmpty(path); return this.storage.ReadAllBytesAsync(path, forceBackup, this.plugin.EffectiveWorkingPluginId); } + + /// + public void DisposeService() + { + Task[] tasksSnapshot; + lock (this.pendingLock) + { + // Mark disposing to reject new writes. + this.isDisposing = true; + + if (this.pendingWrites.Count == 0) + return; + + tasksSnapshot = this.pendingWrites.ToArray(); + } + + try + { + // Wait for all pending writes to complete. If some complete while we're waiting they will be in tasksSnapshot + // and are observed here; newly started writes are rejected due to isDisposing. + Task.WaitAll(tasksSnapshot); + } + catch (AggregateException) + { + // Swallow exceptions here: the underlying write failures will have been surfaced earlier to callers. + // We don't want dispose to throw and crash unload sequences. + } + } } From 28941cb69e71b8d229e5a332ba7edb8c97dcd6d1 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 21:58:13 +0100 Subject: [PATCH 190/477] SelfTestRegistryPluginScoped should inherit from IDalamudService --- .../Plugin/SelfTest/Internal/SelfTestRegistryPluginScoped.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistryPluginScoped.cs b/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistryPluginScoped.cs index 18c518879..e3835ddbc 100644 --- a/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistryPluginScoped.cs +++ b/Dalamud/Plugin/SelfTest/Internal/SelfTestRegistryPluginScoped.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; namespace Dalamud.Plugin.SelfTest.Internal; @@ -12,7 +13,7 @@ namespace Dalamud.Plugin.SelfTest.Internal; [PluginInterface] [ServiceManager.ScopedService] [ResolveVia] -internal class SelfTestRegistryPluginScoped : ISelfTestRegistry, IInternalDisposableService +internal class SelfTestRegistryPluginScoped : ISelfTestRegistry, IInternalDisposableService, IDalamudService { [ServiceManager.ServiceDependency] private readonly SelfTestRegistry selfTestRegistry = Service.Get(); From 0daca30203920fb733c2ab7efc3849216e28694c Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 23:17:21 +0100 Subject: [PATCH 191/477] Make IReliableFileStorage experimental, add legal diagnostic IDs --- Dalamud/Plugin/Services/IReliableFileStorage.cs | 2 ++ Dalamud/Plugin/Services/IUnlockState.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Services/IReliableFileStorage.cs b/Dalamud/Plugin/Services/IReliableFileStorage.cs index 93c2df243..757493a20 100644 --- a/Dalamud/Plugin/Services/IReliableFileStorage.cs +++ b/Dalamud/Plugin/Services/IReliableFileStorage.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; using System.Threading.Tasks; @@ -20,6 +21,7 @@ namespace Dalamud.Plugin.Services; /// /// Saved configuration data using the class uses this functionality implicitly. /// +[Experimental("Dalamud001")] public interface IReliableFileStorage : IDalamudService { /// diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index a0d733f55..0409843c4 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -10,7 +10,7 @@ namespace Dalamud.Plugin.Services; /// /// Interface for determining unlock state of various content in the game. /// -[Experimental("UnlockState")] +[Experimental("Dalamud001")] public interface IUnlockState : IDalamudService { /// From 71927a8bf6fc0135e59fbd2e9e515aacb5e7d75f Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 18 Nov 2025 15:18:16 -0800 Subject: [PATCH 192/477] feat: Add unix sockets - Unix sockets run parallel to Named Pipes - Named Pipes will only run on non-Wine - If the game crashes, the next run will clean up an orphaned socket. - Restructure RPC to be a bit tidier --- .../{Pipes => Rpc}/DalamudUriTests.cs | 5 +- .../Networking/Pipes/Rpc/RpcHostService.cs | 49 ---- .../{Pipes => Rpc}/Api/PluginLinkHandler.cs | 6 +- .../{Pipes => Rpc/Model}/DalamudUri.cs | 2 +- .../{Pipes => }/Rpc/RpcConnection.cs | 23 +- Dalamud/Networking/Rpc/RpcHostService.cs | 105 +++++++++ .../{Pipes => }/Rpc/RpcServiceRegistry.cs | 2 +- .../Service}/ClientHelloService.cs | 3 +- .../Service}/LinkHandlerService.cs | 4 +- .../Networking/Rpc/Transport/IRpcTransport.cs | 32 +++ .../Transport/PipeRpcTransport.cs} | 30 +-- .../Rpc/Transport/UnixRpcTransport.cs | 223 ++++++++++++++++++ Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +- Dalamud/Utility/UnixSocketUtil.cs | 92 ++++++++ 14 files changed, 487 insertions(+), 91 deletions(-) rename Dalamud.Test/{Pipes => Rpc}/DalamudUriTests.cs (98%) delete mode 100644 Dalamud/Networking/Pipes/Rpc/RpcHostService.cs rename Dalamud/Networking/{Pipes => Rpc}/Api/PluginLinkHandler.cs (93%) rename Dalamud/Networking/{Pipes => Rpc/Model}/DalamudUri.cs (98%) rename Dalamud/Networking/{Pipes => }/Rpc/RpcConnection.cs (76%) create mode 100644 Dalamud/Networking/Rpc/RpcHostService.cs rename Dalamud/Networking/{Pipes => }/Rpc/RpcServiceRegistry.cs (98%) rename Dalamud/Networking/{Pipes/Internal => Rpc/Service}/ClientHelloService.cs (97%) rename Dalamud/Networking/{Pipes/Internal => Rpc/Service}/LinkHandlerService.cs (97%) create mode 100644 Dalamud/Networking/Rpc/Transport/IRpcTransport.cs rename Dalamud/Networking/{Pipes/Rpc/PipeRpcHost.cs => Rpc/Transport/PipeRpcTransport.cs} (81%) create mode 100644 Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs create mode 100644 Dalamud/Utility/UnixSocketUtil.cs diff --git a/Dalamud.Test/Pipes/DalamudUriTests.cs b/Dalamud.Test/Rpc/DalamudUriTests.cs similarity index 98% rename from Dalamud.Test/Pipes/DalamudUriTests.cs rename to Dalamud.Test/Rpc/DalamudUriTests.cs index 4977f3814..b371a5698 100644 --- a/Dalamud.Test/Pipes/DalamudUriTests.cs +++ b/Dalamud.Test/Rpc/DalamudUriTests.cs @@ -1,10 +1,11 @@ using System; using System.Linq; -using Dalamud.Networking.Pipes; +using Dalamud.Networking.Rpc.Model; + using Xunit; -namespace Dalamud.Test.Pipes +namespace Dalamud.Test.Rpc { public class DalamudUriTests { diff --git a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs deleted file mode 100644 index 78df27323..000000000 --- a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Dalamud.Logging.Internal; - -namespace Dalamud.Networking.Pipes.Rpc; - -/// -/// The Dalamud service repsonsible for hosting the RPC. -/// -[ServiceManager.EarlyLoadedService] -internal class RpcHostService : IServiceType, IInternalDisposableService -{ - private readonly ModuleLog log = new("RPC"); - private readonly PipeRpcHost host; - - /// - /// Initializes a new instance of the class. - /// - [ServiceManager.ServiceConstructor] - public RpcHostService() - { - this.host = new PipeRpcHost(); - this.host.Start(); - - this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName); - } - - /// - /// Gets the RPC host to drill down. - /// - public PipeRpcHost Host => this.host; - - /// - /// Add a new service Object to the RPC host. - /// - /// The object to add. - public void AddService(object service) => this.host.AddService(service); - - /// - /// Add a new standalone method to the RPC host. - /// - /// The method name to add. - /// The handler to add. - public void AddMethod(string name, Delegate handler) => this.host.AddMethod(name, handler); - - /// - public void DisposeService() - { - this.host.Dispose(); - } -} diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs similarity index 93% rename from Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs rename to Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs index 78fbb0d82..e9372bf0e 100644 --- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs @@ -3,12 +3,14 @@ using Dalamud.Console; using Dalamud.IoC; using Dalamud.IoC.Internal; -using Dalamud.Networking.Pipes.Internal; +using Dalamud.Networking.Rpc.Model; +using Dalamud.Networking.Rpc.Service; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; + #pragma warning disable DAL_RPC -namespace Dalamud.Networking.Pipes.Api; +namespace Dalamud.Networking.Rpc.Api; /// [PluginInterface] diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Rpc/Model/DalamudUri.cs similarity index 98% rename from Dalamud/Networking/Pipes/DalamudUri.cs rename to Dalamud/Networking/Rpc/Model/DalamudUri.cs index 7e639cbbe..852478762 100644 --- a/Dalamud/Networking/Pipes/DalamudUri.cs +++ b/Dalamud/Networking/Rpc/Model/DalamudUri.cs @@ -2,7 +2,7 @@ using System.Collections.Specialized; using System.Web; -namespace Dalamud.Networking.Pipes; +namespace Dalamud.Networking.Rpc.Model; /// /// A Dalamud Uri, in the format: diff --git a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs b/Dalamud/Networking/Rpc/RpcConnection.cs similarity index 76% rename from Dalamud/Networking/Pipes/Rpc/RpcConnection.cs rename to Dalamud/Networking/Rpc/RpcConnection.cs index 8e1c3a085..5288948eb 100644 --- a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs +++ b/Dalamud/Networking/Rpc/RpcConnection.cs @@ -1,34 +1,37 @@ -using System.IO.Pipes; +using System.IO; using System.Threading; using System.Threading.Tasks; +using Dalamud.Networking.Rpc.Service; + using Serilog; + using StreamJsonRpc; -namespace Dalamud.Networking.Pipes.Rpc; +namespace Dalamud.Networking.Rpc; /// -/// A single RPC client session connected via named pipe. +/// A single RPC client session connected via a stream (named pipe or Unix socket). /// internal class RpcConnection : IDisposable { - private readonly NamedPipeServerStream pipe; + private readonly Stream stream; private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); /// /// Initializes a new instance of the class. /// - /// The named pipe that this connection will handle. + /// The stream that this connection will handle. /// A registry of RPC services. - public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry) + public RpcConnection(Stream stream, RpcServiceRegistry registry) { this.Id = Guid.CreateVersion7(); - this.pipe = pipe; + this.stream = stream; this.registry = registry; var formatter = new JsonMessageFormatter(); - var handler = new HeaderDelimitedMessageHandler(pipe, pipe, formatter); + var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter); this.Rpc = new JsonRpc(handler); this.Rpc.AllowModificationWhileListening = true; @@ -72,11 +75,11 @@ internal class RpcConnection : IDisposable try { - this.pipe.Dispose(); + this.stream.Dispose(); } catch (Exception ex) { - Log.Debug(ex, "Error disposing pipe for client {Id}", this.Id); + Log.Debug(ex, "Error disposing stream for client {Id}", this.Id); } this.cts.Dispose(); diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs new file mode 100644 index 000000000..f164992eb --- /dev/null +++ b/Dalamud/Networking/Rpc/RpcHostService.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Networking.Rpc.Transport; +using Dalamud.Utility; + +namespace Dalamud.Networking.Rpc; + +/// +/// The Dalamud service repsonsible for hosting the RPC. +/// +[ServiceManager.EarlyLoadedService] +internal class RpcHostService : IServiceType, IInternalDisposableService +{ + private readonly ModuleLog log = new("RPC"); + private readonly RpcServiceRegistry registry = new(); + private readonly List transports = []; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public RpcHostService() + { + this.StartUnixTransport(); + this.StartPipeTransport(); + + if (this.transports.Count == 0) + { + this.log.Warning("No RPC hosts could be started on this platform"); + } + } + + /// + /// Gets all active RPC transports. + /// + public IReadOnlyList Transports => this.transports; + + /// + /// Add a new service Object to the RPC host. + /// + /// The object to add. + public void AddService(object service) => this.registry.AddService(service); + + /// + /// Add a new standalone method to the RPC host. + /// + /// The method name to add. + /// The handler to add. + public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); + + /// + public void DisposeService() + { + foreach (var host in this.transports) + { + host.Dispose(); + } + + this.transports.Clear(); + } + + /// + public async Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) + { + var clients = this.transports.SelectMany(t => t.Connections).ToImmutableDictionary(); + + if (!clients.TryGetValue(clientId, out var session)) + throw new KeyNotFoundException($"No client {clientId}"); + + return await session.Rpc.InvokeAsync(method, arguments).ConfigureAwait(false); + } + + /// + public async Task BroadcastNotifyAsync(string method, params object[] arguments) + { + await foreach (var transport in this.transports.ToAsyncEnumerable().ConfigureAwait(false)) + { + await transport.BroadcastNotifyAsync(method, arguments).ConfigureAwait(false); + } + } + + private void StartUnixTransport() + { + var transport = new UnixRpcTransport(this.registry); + this.transports.Add(transport); + transport.Start(); + this.log.Information("RpcHostService started Unix socket host: {Socket}", transport.SocketPath); + } + + private void StartPipeTransport() + { + // Wine doesn't support named pipes. + if (Util.IsWine()) + return; + + var transport = new PipeRpcTransport(this.registry); + this.transports.Add(transport); + transport.Start(); + this.log.Information("RpcHostService started named pipe host: {Pipe}", transport.PipeName); + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs similarity index 98% rename from Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs rename to Dalamud/Networking/Rpc/RpcServiceRegistry.cs index 71037d45e..6daea14bf 100644 --- a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs +++ b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs @@ -3,7 +3,7 @@ using System.Threading; using StreamJsonRpc; -namespace Dalamud.Networking.Pipes.Rpc; +namespace Dalamud.Networking.Rpc; /// /// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session. diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs similarity index 97% rename from Dalamud/Networking/Pipes/Internal/ClientHelloService.cs rename to Dalamud/Networking/Rpc/Service/ClientHelloService.cs index 9c182561e..041bc135f 100644 --- a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs +++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs @@ -3,12 +3,11 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; -using Dalamud.Networking.Pipes.Rpc; using Dalamud.Utility; using Lumina.Excel.Sheets; -namespace Dalamud.Networking.Pipes.Internal; +namespace Dalamud.Networking.Rpc.Service; /// /// A minimal service to respond with information about this client. diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs similarity index 97% rename from Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs rename to Dalamud/Networking/Rpc/Service/LinkHandlerService.cs index 3cc4af9f4..9fa311ede 100644 --- a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs +++ b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using Dalamud.Logging.Internal; -using Dalamud.Networking.Pipes.Rpc; +using Dalamud.Networking.Rpc.Model; using Dalamud.Utility; -namespace Dalamud.Networking.Pipes.Internal; +namespace Dalamud.Networking.Rpc.Service; /// /// A service responsible for handling Dalamud URIs and dispatching them accordingly. diff --git a/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs new file mode 100644 index 000000000..ad7578eb4 --- /dev/null +++ b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Dalamud.Networking.Rpc.Transport; + +/// +/// Interface for RPC host implementations (named pipes or Unix sockets). +/// +internal interface IRpcTransport : IDisposable +{ + /// + /// Gets a list of active RPC connections. + /// + IReadOnlyDictionary Connections { get; } + + /// Starts accepting client connections. + void Start(); + + /// Invoke an RPC request on a specific client expecting a result. + /// The client ID to invoke. + /// The method to invoke. + /// Any arguments to invoke. + /// An optional return based on the specified RPC. + /// The expected response type. + Task InvokeClientAsync(Guid clientId, string method, params object[] arguments); + + /// Send a notification to all connected clients (no response expected). + /// The method name to broadcast. + /// The arguments to broadcast. + /// Returns a Task when completed. + Task BroadcastNotifyAsync(string method, params object[] arguments); +} diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs similarity index 81% rename from Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs rename to Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs index ad1cc72cd..0cefeb853 100644 --- a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs +++ b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs @@ -9,26 +9,28 @@ using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Utility; -namespace Dalamud.Networking.Pipes.Rpc; +namespace Dalamud.Networking.Rpc.Transport; /// /// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc. /// -internal class PipeRpcHost : IDisposable +internal class PipeRpcTransport : IRpcTransport { private readonly ModuleLog log = new("RPC/Host"); - private readonly RpcServiceRegistry registry = new(); + private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); private readonly ConcurrentDictionary sessions = new(); private Task? acceptLoopTask; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The RPC service registry to use. /// The pipe name to create. - public PipeRpcHost(string? pipeName = null) + public PipeRpcTransport(RpcServiceRegistry registry, string? pipeName = null) { + this.registry = registry; // Default pipe name based on current process ID for uniqueness per Dalamud instance. this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}"; } @@ -38,16 +40,8 @@ internal class PipeRpcHost : IDisposable /// public string PipeName { get; } - /// Adds a local object exposing RPC methods callable by clients. - /// An arbitrary service object that will be introspected to add to RPC. - public void AddService(object service) => this.registry.AddService(service); - - /// - /// Adds a standalone JSON-RPC method callable by clients. - /// - /// The name to add. - /// The delegate that acts as the handler. - public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); + /// + public IReadOnlyDictionary Connections => this.sessions; /// Starts accepting client connections. public void Start() @@ -86,12 +80,6 @@ internal class PipeRpcHost : IDisposable return Task.WhenAll(tasks); } - /// - /// Gets a list of connected client IDs. - /// - /// Connected client IDs. - public IReadOnlyCollection GetClientIds() => this.sessions.Keys.AsReadOnlyCollection(); - /// public void Dispose() { diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs new file mode 100644 index 000000000..3019f5aaf --- /dev/null +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -0,0 +1,223 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Networking.Rpc.Transport; + +/// +/// Simple multi-client JSON-RPC Unix socket host using StreamJsonRpc. +/// +internal class UnixRpcTransport : IRpcTransport +{ + private readonly ModuleLog log = new("RPC/UnixHost"); + + private readonly RpcServiceRegistry registry; + private readonly CancellationTokenSource cts = new(); + private readonly ConcurrentDictionary sessions = new(); + private readonly string? cleanupSocketDirectory; + + private Task? acceptLoopTask; + private Socket? listenSocket; + + /// + /// Initializes a new instance of the class. + /// + /// The RPC service registry to use. + /// The Unix socket path to create. If null, defaults to a path based on process ID. + public UnixRpcTransport(RpcServiceRegistry registry, string? socketPath = null) + { + this.registry = registry; + + if (socketPath != null) + { + this.SocketPath = socketPath; + } + else + { + var dalamudConfigPath = Service.Get().StartInfo.ConfigurationPath; + var dalamudHome = Path.GetDirectoryName(dalamudConfigPath); + var socketName = $"DalamudRPC.{Environment.ProcessId}.sock"; + + if (dalamudHome == null) + { + this.SocketPath = Path.Combine(Path.GetTempPath(), socketName); + this.log.Warning("Dalamud home is empty! UDS socket will be in temp."); + } + else + { + this.SocketPath = Path.Combine(dalamudHome, socketName); + this.cleanupSocketDirectory = dalamudHome; + } + } + } + + /// + /// Gets the path of the Unix socket this RPC host is using. + /// + public string SocketPath { get; } + + /// + public IReadOnlyDictionary Connections => this.sessions; + + /// Starts accepting client connections. + public void Start() + { + if (this.acceptLoopTask != null) return; + + // Make the directory for the socket if it doesn't exist + var socketDir = Path.GetDirectoryName(this.SocketPath); + if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir)) + { + try + { + Directory.CreateDirectory(socketDir); + } + catch (Exception ex) + { + this.log.Error(ex, "Failed to create socket directory: {Path}", socketDir); + return; + } + } + + // Delete existing socket for this PID, if it exists. + if (File.Exists(this.SocketPath)) + { + try + { + File.Delete(this.SocketPath); + } + catch (Exception ex) + { + this.log.Warning(ex, "Failed to delete existing socket file: {Path}", this.SocketPath); + } + } + + this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); + + // note: needs to be run _after_ we're alive so that we don't delete our own socket. + if (this.cleanupSocketDirectory != null) + { + Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory)); + } + } + + /// Invoke an RPC request on a specific client expecting a result. + /// The client ID to invoke. + /// The method to invoke. + /// Any arguments to invoke. + /// An optional return based on the specified RPC. + /// The expected response type. + public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) + { + if (!this.sessions.TryGetValue(clientId, out var session)) + throw new KeyNotFoundException($"No client {clientId}"); + + return session.Rpc.InvokeAsync(method, arguments); + } + + /// Send a notification to all connected clients (no response expected). + /// The method name to broadcast. + /// The arguments to broadcast. + /// Returns a Task when completed. + public Task BroadcastNotifyAsync(string method, params object[] arguments) + { + var list = this.sessions.Values; + var tasks = new List(list.Count); + foreach (var s in list) + { + tasks.Add(s.Rpc.NotifyAsync(method, arguments)); + } + + return Task.WhenAll(tasks); + } + + /// + public void Dispose() + { + this.cts.Cancel(); + this.acceptLoopTask?.Wait(1000); + + foreach (var kv in this.sessions) + { + kv.Value.Dispose(); + } + + this.sessions.Clear(); + + this.listenSocket?.Dispose(); + + if (File.Exists(this.SocketPath)) + { + try + { + File.Delete(this.SocketPath); + } + catch (Exception ex) + { + this.log.Warning(ex, "Failed to delete socket file on dispose: {Path}", this.SocketPath); + } + } + + this.cts.Dispose(); + this.log.Information("UnixRpcHost disposed ({Socket})", this.SocketPath); + GC.SuppressFinalize(this); + } + + private async Task AcceptLoopAsync() + { + this.log.Information("UnixRpcHost starting on socket {Socket}", this.SocketPath); + var token = this.cts.Token; + + try + { + var endpoint = new UnixDomainSocketEndPoint(this.SocketPath); + this.listenSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + this.listenSocket.Bind(endpoint); + this.listenSocket.Listen(128); + + while (!token.IsCancellationRequested) + { + Socket? clientSocket = null; + try + { + clientSocket = await this.listenSocket.AcceptAsync(token).ConfigureAwait(false); + + var stream = new NetworkStream(clientSocket, ownsSocket: true); + var session = new RpcConnection(stream, this.registry); + this.sessions.TryAdd(session.Id, session); + + this.log.Debug("RPC connection created: {Id}", session.Id); + + _ = session.Completion.ContinueWith(t => + { + this.sessions.TryRemove(session.Id, out _); + this.log.Debug("RPC connection removed: {Id}", session.Id); + }, TaskScheduler.Default); + } + catch (OperationCanceledException) + { + clientSocket?.Dispose(); + break; + } + catch (Exception ex) + { + clientSocket?.Dispose(); + this.log.Error(ex, "Error in socket accept loop"); + await Task.Delay(500, token).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + this.log.Error(ex, "Fatal error in Unix socket accept loop"); + } + } +} diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 5d2d32728..bff5c8ba2 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using Dalamud.Networking.Pipes; +using Dalamud.Networking.Rpc.Model; namespace Dalamud.Plugin.Services; diff --git a/Dalamud/Utility/UnixSocketUtil.cs b/Dalamud/Utility/UnixSocketUtil.cs new file mode 100644 index 000000000..46bb05c74 --- /dev/null +++ b/Dalamud/Utility/UnixSocketUtil.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Net.Sockets; +using System.Threading.Tasks; + +using Serilog; + +namespace Dalamud.Utility; + +/// +/// A set of utilities to help manage Unix sockets. +/// +internal static class UnixSocketUtil +{ + // Default probe timeout in milliseconds. + private const int DefaultProbeMs = 200; + + /// + /// Test whether a Unix socket is alive/listening. + /// + /// The path to test. + /// How long to wait for a connection success. + /// A task result representing if a socket is alive or not. + public static async Task IsSocketAlive(string path, int timeoutMs = DefaultProbeMs) + { + if (string.IsNullOrEmpty(path)) return false; + var endpoint = new UnixDomainSocketEndPoint(path); + using var client = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + + var connectTask = client.ConnectAsync(endpoint); + var completed = await Task.WhenAny(connectTask, Task.Delay(timeoutMs)).ConfigureAwait(false); + + if (completed == connectTask) + { + // Connected or failed very quickly. If the task is successful, the socket is alive. + if (connectTask.IsCompletedSuccessfully) + { + try + { + client.Shutdown(SocketShutdown.Both); + } + catch + { + // ignored + } + + return true; + } + } + + return false; + } + + /// + /// Find and remove stale Dalamud RPC sockets. + /// + /// The directory to scan for stale sockets. + /// The timeout to wait for a connection attempt to succeed. + /// A task that executes when sockets are purged. + public static async Task CleanStaleSockets(string directory, int probeTimeoutMs = DefaultProbeMs) + { + if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return; + + foreach (var file in Directory.EnumerateFiles(directory, "DalamudRPC.*.sock", SearchOption.TopDirectoryOnly)) + { + // we don't need to check ourselves. + if (file.Contains(Environment.ProcessId.ToString())) continue; + + bool shouldDelete; + + try + { + shouldDelete = !await IsSocketAlive(file, probeTimeoutMs); + } + catch + { + shouldDelete = true; + } + + if (shouldDelete) + { + try + { + File.Delete(file); + } + catch (Exception ex) + { + Log.Error(ex, "Could not delete stale socket file: {File}", file); + } + } + } + } +} From 01d8fc0c7ea177bdbe46ec5ebf8c9cd910556852 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 18 Nov 2025 15:57:37 -0800 Subject: [PATCH 193/477] fix: log tweaks - also fix a boot failure --- Dalamud/Networking/Rpc/RpcHostService.cs | 4 ++-- Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs | 3 +-- Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs | 3 +-- Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs | 4 +++- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs index f164992eb..60152b355 100644 --- a/Dalamud/Networking/Rpc/RpcHostService.cs +++ b/Dalamud/Networking/Rpc/RpcHostService.cs @@ -88,7 +88,7 @@ internal class RpcHostService : IServiceType, IInternalDisposableService var transport = new UnixRpcTransport(this.registry); this.transports.Add(transport); transport.Start(); - this.log.Information("RpcHostService started Unix socket host: {Socket}", transport.SocketPath); + this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath); } private void StartPipeTransport() @@ -100,6 +100,6 @@ internal class RpcHostService : IServiceType, IInternalDisposableService var transport = new PipeRpcTransport(this.registry); this.transports.Add(transport); transport.Start(); - this.log.Information("RpcHostService started named pipe host: {Pipe}", transport.PipeName); + this.log.Information("RpcHostService listening to named pipe: {Pipe}", transport.PipeName); } } diff --git a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs index 0cefeb853..727eb9125 100644 --- a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs +++ b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs @@ -16,7 +16,7 @@ namespace Dalamud.Networking.Rpc.Transport; /// internal class PipeRpcTransport : IRpcTransport { - private readonly ModuleLog log = new("RPC/Host"); + private readonly ModuleLog log = new("RPC/Transport/NamedPipe"); private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); @@ -107,7 +107,6 @@ internal class PipeRpcTransport : IRpcTransport private async Task AcceptLoopAsync() { - this.log.Information("PipeRpcHost starting on pipe {Pipe}", this.PipeName); var token = this.cts.Token; var security = this.BuildPipeSecurity(); diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs index 3019f5aaf..e1ef64f76 100644 --- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -17,7 +17,7 @@ namespace Dalamud.Networking.Rpc.Transport; /// internal class UnixRpcTransport : IRpcTransport { - private readonly ModuleLog log = new("RPC/UnixHost"); + private readonly ModuleLog log = new("RPC/Transport/UnixSocket"); private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); @@ -173,7 +173,6 @@ internal class UnixRpcTransport : IRpcTransport private async Task AcceptLoopAsync() { - this.log.Information("UnixRpcHost starting on socket {Socket}", this.SocketPath); var token = this.cts.Token; try diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs index af3b583c9..7e9faf3f9 100644 --- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs +++ b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using Dalamud.Plugin.Services; + namespace Dalamud.Plugin.SelfTest; /// @@ -44,7 +46,7 @@ namespace Dalamud.Plugin.SelfTest; /// } /// /// -public interface ISelfTestRegistry +public interface ISelfTestRegistry : IDalamudService { /// /// Registers the self-test steps for this plugin. diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index bff5c8ba2..37101222a 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services; /// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// [Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] -public interface IPluginLinkHandler +public interface IPluginLinkHandler : IDalamudService { /// /// A delegate containing the received URI. From 0d8f577576800d9e00bdf262562f4ba4c3e910ad Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 18 Nov 2025 16:28:03 -0800 Subject: [PATCH 194/477] feat: add debug link handler as demo --- .../Rpc/Service/Links/DebugLinkHandler.cs | 67 +++++++++++++++++++ .../Links}/PluginLinkHandler.cs | 3 +- 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs rename Dalamud/Networking/Rpc/{Api => Service/Links}/PluginLinkHandler.cs (95%) diff --git a/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs new file mode 100644 index 000000000..269617fc0 --- /dev/null +++ b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs @@ -0,0 +1,67 @@ +using Dalamud.Game.Gui.Toast; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Networking.Rpc.Model; + +namespace Dalamud.Networking.Rpc.Service.Links; + +#if DEBUG + +/// +/// A debug controller for link handling. +/// +[ServiceManager.EarlyLoadedService] +internal sealed class DebugLinkHandler : IInternalDisposableService +{ + private readonly LinkHandlerService linkHandlerService; + + /// + /// Initializes a new instance of the class. + /// + /// Injected LinkHandler. + [ServiceManager.ServiceConstructor] + public DebugLinkHandler(LinkHandlerService linkHandler) + { + this.linkHandlerService = linkHandler; + + this.linkHandlerService.Register("debug", this.HandleLink); + } + + /// + public void DisposeService() + { + this.linkHandlerService.Unregister("debug", this.HandleLink); + } + + private void HandleLink(DalamudUri uri) + { + var action = uri.Path.Split("/").GetValue(1)?.ToString(); + switch (action) + { + case "toast": + this.ShowToast(uri); + break; + case "notification": + this.ShowNotification(uri); + break; + } + } + + private void ShowToast(DalamudUri uri) + { + var message = uri.QueryParams.Get("message") ?? "Hello, world!"; + Service.Get().ShowNormal(message); + } + + private void ShowNotification(DalamudUri uri) + { + Service.Get().AddNotification( + new Notification + { + Title = uri.QueryParams.Get("title"), + Content = uri.QueryParams.Get("content") ?? "Hello, world!", + }); + } +} + +#endif diff --git a/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs similarity index 95% rename from Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs rename to Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs index e9372bf0e..4dbe3fdf1 100644 --- a/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs @@ -4,13 +4,12 @@ using Dalamud.Console; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Networking.Rpc.Model; -using Dalamud.Networking.Rpc.Service; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; #pragma warning disable DAL_RPC -namespace Dalamud.Networking.Rpc.Api; +namespace Dalamud.Networking.Rpc.Service.Links; /// [PluginInterface] From 0656bff1f9bd582fbaaef89e81f761580b1bec55 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 20 Nov 2025 19:31:50 +0100 Subject: [PATCH 195/477] Fix experimental diagnostic IDs --- Dalamud/Game/UnlockState/UnlockState.cs | 2 +- Dalamud/Storage/ReliableFileStoragePluginScoped.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index a4b9381cc..cd896ffb6 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -22,7 +22,7 @@ using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; namespace Dalamud.Game.UnlockState; -#pragma warning disable UnlockState +#pragma warning disable Dalamud001 /// /// This class provides unlock state of various content in the game. diff --git a/Dalamud/Storage/ReliableFileStoragePluginScoped.cs b/Dalamud/Storage/ReliableFileStoragePluginScoped.cs index f6598a087..59d6bccc4 100644 --- a/Dalamud/Storage/ReliableFileStoragePluginScoped.cs +++ b/Dalamud/Storage/ReliableFileStoragePluginScoped.cs @@ -11,6 +11,8 @@ using Dalamud.Plugin.Services; namespace Dalamud.Storage; +#pragma warning disable Dalamud001 + /// /// Plugin-scoped VFS wrapper. /// From ea07f41ab14dffb93b9ef9cac00f4d8a9f8d8e03 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 23 Nov 2025 23:47:28 +0100 Subject: [PATCH 196/477] Update ClientStructs (#2465) 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 0afa6b672..0769d1f18 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 0afa6b67288e5e667da74c1d3ad582e6c964644c +Subproject commit 0769d1f180f859688f47a7a99610e9ce10da946c From 1f30ce4c396287bf504fc2228aaf8f14eefa150d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 25 Nov 2025 18:41:21 +0100 Subject: [PATCH 197/477] Require TestingDalamudApiLevel to be set for testing --- Dalamud/Plugin/Internal/PluginManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index fd5c048c7..e2eded57c 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -362,6 +362,9 @@ internal class PluginManager : IInternalDisposableService if (!this.configuration.DoPluginTest) return false; + if (!manifest.TestingDalamudApiLevel.HasValue) + return false; + return manifest.IsTestingExclusive || manifest.IsAvailableForTesting; } From 7b286c427cbd14859381ce7ee99bef1d9768e033 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Nov 2025 10:08:24 -0800 Subject: [PATCH 198/477] chore: remove named pipe transport, use startinfo for pathing --- Dalamud.Boot/DalamudStartInfo.cpp | 5 + Dalamud.Boot/DalamudStartInfo.h | 1 + Dalamud.Boot/veh.cpp | 15 +- Dalamud.Common/DalamudStartInfo.cs | 6 + Dalamud.Injector/Program.cs | 6 + Dalamud/Networking/Rpc/RpcHostService.cs | 14 -- .../Rpc/Transport/PipeRpcTransport.cs | 154 ------------------ .../Rpc/Transport/UnixRpcTransport.cs | 34 ++-- .../Rpc}/UnixSocketUtil.cs | 2 +- 9 files changed, 41 insertions(+), 196 deletions(-) delete mode 100644 Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs rename Dalamud/{Utility => Networking/Rpc}/UnixSocketUtil.cs (98%) diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 5be8f97d0..9c8fd9721 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) { config.LogName = json.value("LogName", config.LogName); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory); + + if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) { + config.TempDirectory = json.value("TempDirectory", config.TempDirectory); + } + config.Language = json.value("Language", config.Language); config.Platform = json.value("Platform", config.Platform); config.GameVersion = json.value("GameVersion", config.GameVersion); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 0eeaddeed..308dcab7d 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -44,6 +44,7 @@ struct DalamudStartInfo { std::string ConfigurationPath; std::string LogPath; std::string LogName; + std::string TempDirectory; std::string PluginDirectory; std::string AssetDirectory; ClientLanguage Language = ClientLanguage::English; diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index b0ec1cefa..b75256af8 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -122,6 +122,7 @@ static DalamudExpected append_injector_launch_args(std::vector(g_startInfo.LogName) + L"\""); args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert(g_startInfo.PluginDirectory) + L"\""); args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert(g_startInfo.AssetDirectory) + L"\""); + args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert(g_startInfo.TempDirectory) + L"\""); args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast(g_startInfo.Language))); args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs)); // NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler @@ -268,7 +269,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) && !is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip)) - return EXCEPTION_CONTINUE_SEARCH; + return EXCEPTION_CONTINUE_SEARCH; } return exception_handler(ex); @@ -297,7 +298,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536)) { hWritePipe.emplace(hWritePipeRaw, &CloseHandle); - + if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) { hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle); @@ -315,9 +316,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } // additional information - STARTUPINFOEXW siex{}; + STARTUPINFOEXW siex{}; PROCESS_INFORMATION pi{}; - + siex.StartupInfo.cb = sizeof siex; siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE; @@ -385,7 +386,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) argstr.push_back(L' '); } argstr.pop_back(); - + if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr)) { logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError()); @@ -400,7 +401,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) TRUE, // Set handle inheritance to FALSE EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W) nullptr, // Use parent's environment block - nullptr, // Use parent's starting directory + nullptr, // Use parent's starting directory &siex.StartupInfo, // Pointer to STARTUPINFO structure &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) )) @@ -416,7 +417,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } CloseHandle(pi.hThread); - + g_crashhandler_process = pi.hProcess; g_crashhandler_pipe_write = hWritePipe->release(); logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId); diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index a0d7f8b0b..8c66a85ba 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -34,6 +34,12 @@ public record DalamudStartInfo /// public string? ConfigurationPath { get; set; } + /// + /// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user. + /// It should also be predictable and easy for launchers to find. + /// + public string? TempDirectory { get; set; } + /// /// Gets or sets the path of the log files. /// diff --git a/Dalamud.Injector/Program.cs b/Dalamud.Injector/Program.cs index e224791e6..13fcacef2 100644 --- a/Dalamud.Injector/Program.cs +++ b/Dalamud.Injector/Program.cs @@ -291,6 +291,7 @@ namespace Dalamud.Injector var configurationPath = startInfo.ConfigurationPath; var pluginDirectory = startInfo.PluginDirectory; var assetDirectory = startInfo.AssetDirectory; + var tempDirectory = startInfo.TempDirectory; var delayInitializeMs = startInfo.DelayInitializeMs; var logName = startInfo.LogName; var logPath = startInfo.LogPath; @@ -321,6 +322,10 @@ namespace Dalamud.Injector { assetDirectory = args[i][key.Length..]; } + else if (args[i].StartsWith(key = "--dalamud-temp-directory=")) + { + tempDirectory = args[i][key.Length..]; + } else if (args[i].StartsWith(key = "--dalamud-delay-initialize=")) { delayInitializeMs = int.Parse(args[i][key.Length..]); @@ -433,6 +438,7 @@ namespace Dalamud.Injector startInfo.ConfigurationPath = configurationPath; startInfo.PluginDirectory = pluginDirectory; startInfo.AssetDirectory = assetDirectory; + startInfo.TempDirectory = tempDirectory; startInfo.Language = clientLanguage; startInfo.Platform = platform; startInfo.DelayInitializeMs = delayInitializeMs; diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs index 60152b355..bbe9dc8eb 100644 --- a/Dalamud/Networking/Rpc/RpcHostService.cs +++ b/Dalamud/Networking/Rpc/RpcHostService.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Networking.Rpc.Transport; -using Dalamud.Utility; namespace Dalamud.Networking.Rpc; @@ -26,7 +25,6 @@ internal class RpcHostService : IServiceType, IInternalDisposableService public RpcHostService() { this.StartUnixTransport(); - this.StartPipeTransport(); if (this.transports.Count == 0) { @@ -90,16 +88,4 @@ internal class RpcHostService : IServiceType, IInternalDisposableService transport.Start(); this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath); } - - private void StartPipeTransport() - { - // Wine doesn't support named pipes. - if (Util.IsWine()) - return; - - var transport = new PipeRpcTransport(this.registry); - this.transports.Add(transport); - transport.Start(); - this.log.Information("RpcHostService listening to named pipe: {Pipe}", transport.PipeName); - } } diff --git a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs deleted file mode 100644 index 727eb9125..000000000 --- a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO.Pipes; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Logging.Internal; -using Dalamud.Utility; - -namespace Dalamud.Networking.Rpc.Transport; - -/// -/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc. -/// -internal class PipeRpcTransport : IRpcTransport -{ - private readonly ModuleLog log = new("RPC/Transport/NamedPipe"); - - private readonly RpcServiceRegistry registry; - private readonly CancellationTokenSource cts = new(); - private readonly ConcurrentDictionary sessions = new(); - private Task? acceptLoopTask; - - /// - /// Initializes a new instance of the class. - /// - /// The RPC service registry to use. - /// The pipe name to create. - public PipeRpcTransport(RpcServiceRegistry registry, string? pipeName = null) - { - this.registry = registry; - // Default pipe name based on current process ID for uniqueness per Dalamud instance. - this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}"; - } - - /// - /// Gets the name of the named pipe this RPC host is using. - /// - public string PipeName { get; } - - /// - public IReadOnlyDictionary Connections => this.sessions; - - /// Starts accepting client connections. - public void Start() - { - if (this.acceptLoopTask != null) return; - this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); - } - - /// Invoke an RPC request on a specific client expecting a result. - /// The client ID to invoke. - /// The method to invoke. - /// Any arguments to invoke. - /// An optional return based on the specified RPC. - /// The expected response type. - public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) - { - if (!this.sessions.TryGetValue(clientId, out var session)) - throw new KeyNotFoundException($"No client {clientId}"); - - return session.Rpc.InvokeAsync(method, arguments); - } - - /// Send a notification to all connected clients (no response expected). - /// The method name to broadcast. - /// The arguments to broadcast. - /// Returns a Task when completed. - public Task BroadcastNotifyAsync(string method, params object[] arguments) - { - var list = this.sessions.Values; - var tasks = new List(list.Count); - foreach (var s in list) - { - tasks.Add(s.Rpc.NotifyAsync(method, arguments)); - } - - return Task.WhenAll(tasks); - } - - /// - public void Dispose() - { - this.cts.Cancel(); - this.acceptLoopTask?.Wait(1000); - - foreach (var kv in this.sessions) - { - kv.Value.Dispose(); - } - - this.sessions.Clear(); - this.cts.Dispose(); - this.log.Information("PipeRpcHost disposed ({Pipe})", this.PipeName); - GC.SuppressFinalize(this); - } - - private PipeSecurity BuildPipeSecurity() - { - var ps = new PipeSecurity(); - ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User!, PipeAccessRights.FullControl, AccessControlType.Allow)); - - return ps; - } - - private async Task AcceptLoopAsync() - { - var token = this.cts.Token; - var security = this.BuildPipeSecurity(); - - while (!token.IsCancellationRequested) - { - NamedPipeServerStream? server = null; - try - { - server = NamedPipeServerStreamAcl.Create( - this.PipeName, - PipeDirection.InOut, - NamedPipeServerStream.MaxAllowedServerInstances, - PipeTransmissionMode.Message, - PipeOptions.Asynchronous, - 65536, - 65536, - security); - - await server.WaitForConnectionAsync(token).ConfigureAwait(false); - - var session = new RpcConnection(server, this.registry); - this.sessions.TryAdd(session.Id, session); - - this.log.Debug("RPC connection created: {Id}", session.Id); - - _ = session.Completion.ContinueWith(t => - { - this.sessions.TryRemove(session.Id, out _); - this.log.Debug("RPC connection removed: {Id}", session.Id); - }, TaskScheduler.Default); - } - catch (OperationCanceledException) - { - server?.Dispose(); - break; - } - catch (Exception ex) - { - server?.Dispose(); - this.log.Error(ex, "Error in pipe accept loop"); - await Task.Delay(500, token).ConfigureAwait(false); - } - } - } -} diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs index e1ef64f76..064ce375d 100644 --- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -31,30 +31,30 @@ internal class UnixRpcTransport : IRpcTransport /// Initializes a new instance of the class. /// /// The RPC service registry to use. - /// The Unix socket path to create. If null, defaults to a path based on process ID. - public UnixRpcTransport(RpcServiceRegistry registry, string? socketPath = null) + /// The Unix socket directory to use. If null, defaults to Dalamud home directory. + /// The name of the socket to create. + public UnixRpcTransport(RpcServiceRegistry registry, string? socketDirectory = null, string? socketName = null) { this.registry = registry; + socketName ??= $"DalamudRPC.{Environment.ProcessId}.sock"; - if (socketPath != null) + if (!socketDirectory.IsNullOrEmpty()) { - this.SocketPath = socketPath; + this.SocketPath = Path.Combine(socketDirectory, socketName); } else { - var dalamudConfigPath = Service.Get().StartInfo.ConfigurationPath; - var dalamudHome = Path.GetDirectoryName(dalamudConfigPath); - var socketName = $"DalamudRPC.{Environment.ProcessId}.sock"; + socketDirectory = Service.Get().StartInfo.TempDirectory; - if (dalamudHome == null) + if (socketDirectory == null) { this.SocketPath = Path.Combine(Path.GetTempPath(), socketName); - this.log.Warning("Dalamud home is empty! UDS socket will be in temp."); + this.log.Warning("Temp dir was not set in StartInfo; using system temp for unix socket."); } else { - this.SocketPath = Path.Combine(dalamudHome, socketName); - this.cleanupSocketDirectory = dalamudHome; + this.SocketPath = Path.Combine(socketDirectory, socketName); + this.cleanupSocketDirectory = socketDirectory; } } } @@ -76,15 +76,8 @@ internal class UnixRpcTransport : IRpcTransport var socketDir = Path.GetDirectoryName(this.SocketPath); if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir)) { - try - { - Directory.CreateDirectory(socketDir); - } - catch (Exception ex) - { - this.log.Error(ex, "Failed to create socket directory: {Path}", socketDir); - return; - } + this.log.Error("Directory for unix socket does not exist: {Path}", socketDir); + return; } // Delete existing socket for this PID, if it exists. @@ -103,6 +96,7 @@ internal class UnixRpcTransport : IRpcTransport this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); // note: needs to be run _after_ we're alive so that we don't delete our own socket. + // TODO: This should *probably* be handed by the launcher instead. if (this.cleanupSocketDirectory != null) { Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory)); diff --git a/Dalamud/Utility/UnixSocketUtil.cs b/Dalamud/Networking/Rpc/UnixSocketUtil.cs similarity index 98% rename from Dalamud/Utility/UnixSocketUtil.cs rename to Dalamud/Networking/Rpc/UnixSocketUtil.cs index 46bb05c74..b7500a946 100644 --- a/Dalamud/Utility/UnixSocketUtil.cs +++ b/Dalamud/Networking/Rpc/UnixSocketUtil.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Serilog; -namespace Dalamud.Utility; +namespace Dalamud.Networking.Rpc; /// /// A set of utilities to help manage Unix sockets. From 8ab7b59ae47f5deaa59dc1001e92855c3b6e2a89 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Nov 2025 10:17:12 -0800 Subject: [PATCH 199/477] fix: Missing service types causing injection failures --- Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs | 4 +++- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs index af3b583c9..7e9faf3f9 100644 --- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs +++ b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using Dalamud.Plugin.Services; + namespace Dalamud.Plugin.SelfTest; /// @@ -44,7 +46,7 @@ namespace Dalamud.Plugin.SelfTest; /// } /// /// -public interface ISelfTestRegistry +public interface ISelfTestRegistry : IDalamudService { /// /// Registers the self-test steps for this plugin. diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 5d2d32728..c05757ac7 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services; /// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// [Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] -public interface IPluginLinkHandler +public interface IPluginLinkHandler : IDalamudService { /// /// A delegate containing the received URI. From d56c7a19630070be274897f3a0ec6819986fdd2a Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 25 Nov 2025 20:31:46 +0100 Subject: [PATCH 200/477] Add auto-generated changelogs through CI --- .github/generate_changelog.py | 211 +++++++++++++++++++++++ .github/workflows/generate-changelog.yml | 46 +++++ 2 files changed, 257 insertions(+) create mode 100644 .github/generate_changelog.py create mode 100644 .github/workflows/generate-changelog.yml diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py new file mode 100644 index 000000000..5e921fd6e --- /dev/null +++ b/.github/generate_changelog.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +Generate a changelog from git commits between the last two tags and post to Discord webhook. +""" + +import subprocess +import re +import sys +import json +import argparse +from typing import List, Tuple, Optional + + +def run_git_command(args: List[str]) -> str: + """Run a git command and return its output.""" + try: + result = subprocess.run( + ["git"] + args, + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Git command failed: {e}", file=sys.stderr) + sys.exit(1) + + +def get_last_two_tags() -> Tuple[str, str]: + """Get the latest two git tags.""" + tags = run_git_command(["tag", "--sort=-version:refname"]) + tag_list = [t for t in tags.split("\n") if t] + + # Filter out old tags that start with 'v' (old versioning scheme) + tag_list = [t for t in tag_list if not t.startswith('v')] + + if len(tag_list) < 2: + print("Error: Need at least 2 tags in the repository", file=sys.stderr) + sys.exit(1) + + return tag_list[0], tag_list[1] + + +def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]: + """Get the commit hash of a submodule at a specific tag.""" + try: + # Get the submodule commit at the specified tag + result = run_git_command(["ls-tree", tag, submodule_path]) + # Format is: " commit \t" + parts = result.split() + if len(parts) >= 3 and parts[1] == "commit": + return parts[2] + return None + except: + return None + + +def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]: + """Get commits between two tags. Returns list of (message, author) tuples.""" + log_output = run_git_command([ + "log", + f"{tag2}..{tag1}", + "--format=%s|%an|%h" + ]) + + commits = [] + for line in log_output.split("\n"): + if "|" in line: + message, author, sha = line.split("|", 2) + commits.append((message.strip(), author.strip(), sha.strip())) + + return commits + + +def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]: + """Filter out commits matching any of the ignore patterns.""" + compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns] + + filtered = [] + for message, author, sha in commits: + if not any(pattern.search(message) for pattern in compiled_patterns): + filtered.append((message, author, sha)) + + return filtered + + +def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]], + cs_commit_new: Optional[str], cs_commit_old: Optional[str]) -> str: + """Generate markdown changelog.""" + # Calculate statistics + commit_count = len(commits) + unique_authors = len(set(author for _, author, _ in commits)) + + changelog = f"# Dalamud Release v{version}\n\n" + changelog += f"We just released Dalamud v{version}, which should be available to users within a few minutes. " + changelog += f"This release includes **{commit_count} commit{'s' if commit_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n" + changelog += f"[Click here]() to see all Dalamud changes.\n\n" + + if cs_commit_new and cs_commit_old and cs_commit_new != cs_commit_old: + changelog += f"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`]()**.\n" + changelog += f"[Click here]() to see all CS changes.\n" + elif cs_commit_new: + changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`]()**.\n" + + changelog += "## Dalamud Changes\n\n" + + for message, author, sha in commits: + changelog += f"* {message} (by **{author}** as [`{sha}`]())\n" + + return changelog + + +def post_to_discord(webhook_url: str, content: str, version: str) -> None: + """Post changelog to Discord webhook as a file attachment.""" + try: + import requests + except ImportError: + print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr) + sys.exit(1) + + filename = f"changelog-v{version}.md" + + # Prepare the payload + data = { + "content": f"Dalamud v{version} has been released!", + "attachments": [ + { + "id": "0", + "filename": filename + } + ] + } + + # Prepare the files + files = { + "payload_json": (None, json.dumps(data)), + "files[0]": (filename, content.encode('utf-8'), 'text/markdown') + } + + try: + result = requests.post(webhook_url, files=files) + result.raise_for_status() + print(f"Successfully posted to Discord webhook, code {result.status_code}") + except requests.exceptions.HTTPError as err: + print(f"Failed to post to Discord: {err}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Failed to post to Discord: {e}", file=sys.stderr) + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser( + description="Generate changelog from git commits and post to Discord webhook" + ) + parser.add_argument( + "--webhook-url", + required=True, + help="Discord webhook URL" + ) + parser.add_argument( + "--ignore", + action="append", + default=[], + help="Regex patterns to ignore commits (can be specified multiple times)" + ) + parser.add_argument( + "--submodule-path", + default="lib/FFXIVClientStructs", + help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)" + ) + + args = parser.parse_args() + + # Get the last two tags + latest_tag, previous_tag = get_last_two_tags() + print(f"Generating changelog between {previous_tag} and {latest_tag}") + + # Get submodule commits at both tags + cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag) + cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag) + + if cs_commit_new: + print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}") + if cs_commit_old: + print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}") + + # Get commits between tags + commits = get_commits_between_tags(latest_tag, previous_tag) + print(f"Found {len(commits)} commits") + + # Filter commits + filtered_commits = filter_commits(commits, args.ignore) + print(f"After filtering: {len(filtered_commits)} commits") + + # Generate changelog + changelog = generate_changelog(latest_tag, previous_tag, filtered_commits, + cs_commit_new, cs_commit_old) + + print("\n" + "="*50) + print("Generated Changelog:") + print("="*50) + print(changelog) + print("="*50 + "\n") + + # Post to Discord + post_to_discord(args.webhook_url, changelog, latest_tag) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml new file mode 100644 index 000000000..5fed3b1eb --- /dev/null +++ b/.github/workflows/generate-changelog.yml @@ -0,0 +1,46 @@ +name: Generate Changelog + +on: + workflow_dispatch: + push: + tags: + - '*' + +jobs: + generate-changelog: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history and tags + submodules: true # Fetch submodules + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests + + - name: Generate and post changelog + run: | + python .github/generate_changelog.py \ + --webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \ + --ignore "^Merge" \ + --ignore "^build:" \ + --ignore "^docs:" + env: + GIT_TERMINAL_PROMPT: 0 + + - name: Upload changelog as artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: changelog + path: changelog-*.md + if-no-files-found: ignore \ No newline at end of file From bd427d7b54d2eb410db345c284fd05bd4269caf1 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 25 Nov 2025 20:32:39 +0100 Subject: [PATCH 201/477] build: 13.0.0.10 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index e45ba8a1e..b5062438a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.9 + 13.0.0.10 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 9a1fae8246d0d55cf9d52b0c31c28956b456c6d8 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 17:27:48 -0800 Subject: [PATCH 202/477] Refactor Addon Lifecycle --- .../Game/Addon/AddonLifecyclePooledArgs.cs | 107 ----- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +- .../AddonArgTypes/AddonFinalizeArgs.cs | 8 +- .../AddonArgTypes/AddonGenericArgs.cs | 18 + .../AddonArgTypes/AddonReceiveEventArgs.cs | 16 +- .../AddonArgTypes/AddonRefreshArgs.cs | 12 +- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 12 +- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 12 +- .../AddonArgTypes/AddonUpdateArgs.cs | 10 +- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +- Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 54 ++- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 298 ++----------- .../AddonLifecycleAddressResolver.cs | 38 +- .../AddonLifecycleReceiveEventListener.cs | 112 ----- .../Game/Addon/Lifecycle/AddonSetupHook.cs | 80 ---- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 405 ++++++++++++++++++ Dalamud/Hooking/Internal/CallHook.cs | 100 ----- .../Data/Widgets/AddonLifecycleWidget.cs | 51 --- 19 files changed, 543 insertions(+), 825 deletions(-) delete mode 100644 Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs delete mode 100644 Dalamud/Hooking/Internal/CallHook.cs diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs deleted file mode 100644 index 14def2036..000000000 --- a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Threading; - -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -namespace Dalamud.Game.Addon; - -/// Argument pool for Addon Lifecycle services. -[ServiceManager.EarlyLoadedService] -internal sealed class AddonLifecyclePooledArgs : IServiceType -{ - private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64]; - private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64]; - private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64]; - private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64]; - private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64]; - private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64]; - private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64]; - - [ServiceManager.ServiceConstructor] - private AddonLifecyclePooledArgs() - { - } - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonRequestedUpdateArgs arg) => - new(out arg, this.addonRequestedUpdateArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonReceiveEventArgs arg) => - new(out arg, this.addonReceiveEventArgPool); - - /// Returns the object to the pool on dispose. - /// The type. - public readonly ref struct PooledEntry - where T : AddonArgs, new() - { - private readonly Span pool; - private readonly T obj; - - /// Initializes a new instance of the struct. - /// An instance of the argument. - /// The pool to rent from and return to. - public PooledEntry(out T arg, Span pool) - { - this.pool = pool; - foreach (ref var item in pool) - { - if (Interlocked.Exchange(ref item, null) is { } v) - { - this.obj = arg = v; - return; - } - } - - this.obj = arg = new(); - } - - /// Returns the item to the pool. - public void Dispose() - { - var tmp = this.obj; - foreach (ref var item in this.pool) - { - if (Interlocked.Exchange(ref item, tmp) is not { } tmp2) - return; - tmp = tmp2; - } - } - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index c008db08f..0b2ae1178 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Base class for AddonLifecycle AddonArgTypes. /// -public abstract unsafe class AddonArgs +public abstract class AddonArgs { /// /// Constant string representing the name of an addon that is invalid. diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 989e11912..7254ba7b3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -3,7 +3,7 @@ /// /// Addon argument data for Draw events. /// -public class AddonDrawArgs : AddonArgs, ICloneable +public class AddonDrawArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -15,10 +15,4 @@ public class AddonDrawArgs : AddonArgs, ICloneable /// public override AddonArgsType Type => AddonArgsType.Draw; - - /// - public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index d9401b414..12def3ad3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonFinalizeArgs : AddonArgs, ICloneable +public class AddonFinalizeArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -15,10 +15,4 @@ public class AddonFinalizeArgs : AddonArgs, ICloneable /// public override AddonArgsType Type => AddonArgsType.Finalize; - - /// - public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs new file mode 100644 index 000000000..f3078af69 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Draw events. +/// +public class AddonGenericArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonGenericArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Generic; +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 980fe4f2f..05f51b118 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonReceiveEventArgs : AddonArgs, ICloneable +public class AddonReceiveEventArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -36,19 +36,13 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable /// public nint Data { get; set; } - /// - public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkEventType = default; - this.EventParam = default; - this.AtkEvent = default; - this.Data = default; + this.AtkEventType = 0; + this.EventParam = 0; + this.AtkEvent = 0; + this.Data = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index d28631c3c..c01c065c1 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Refresh events. /// -public class AddonRefreshArgs : AddonArgs, ICloneable +public class AddonRefreshArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -33,17 +33,11 @@ public class AddonRefreshArgs : AddonArgs, ICloneable /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; + this.AtkValueCount = 0; + this.AtkValues = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index e87a980fd..bf00c5d6e 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for OnRequestedUpdate events. /// -public class AddonRequestedUpdateArgs : AddonArgs, ICloneable +public class AddonRequestedUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -26,17 +26,11 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable /// public nint StringArrayData { get; set; } - /// - public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.NumberArrayData = default; - this.StringArrayData = default; + this.NumberArrayData = 0; + this.StringArrayData = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 0dd9ecee2..9b7e86a61 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Setup events. /// -public class AddonSetupArgs : AddonArgs, ICloneable +public class AddonSetupArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -33,17 +33,11 @@ public class AddonSetupArgs : AddonArgs, ICloneable /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; + this.AtkValueCount = 0; + this.AtkValues = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index a263f6ae4..bab62fc89 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// -public class AddonUpdateArgs : AddonArgs, ICloneable +public class AddonUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -30,16 +30,10 @@ public class AddonUpdateArgs : AddonArgs, ICloneable /// internal float TimeDeltaInternal { get; set; } - /// - public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.TimeDeltaInternal = default; + this.TimeDeltaInternal = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index b58b5f4c7..95dc5f718 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -9,34 +9,39 @@ public enum AddonArgsType /// Contains argument data for Setup. /// Setup, - + /// /// Contains argument data for Update. /// Update, - + /// /// Contains argument data for Draw. - /// + /// Draw, - + /// /// Contains argument data for Finalize. - /// + /// Finalize, - + /// /// Contains argument data for RequestedUpdate. - /// + /// RequestedUpdate, - + /// /// Contains argument data for Refresh. - /// + /// Refresh, - + /// /// Contains argument data for ReceiveEvent. /// ReceiveEvent, + + /// + /// Generic arg type that contains no meaningful data + /// + Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 5fd0ac964..7738d6c6a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -16,7 +16,7 @@ public enum AddonEvent /// /// PreSetup, - + /// /// An event that is fired after an addon has finished its initial setup. This event is particularly useful for /// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data @@ -64,7 +64,7 @@ public enum AddonEvent /// /// PreFinalize, - + /// /// An event that is fired before a call to is made in response to a /// change in the subscribed or @@ -81,13 +81,13 @@ public enum AddonEvent /// to the Free Company's overview. /// PreRequestedUpdate, - + /// /// An event that is fired after an addon has finished processing an ArrayData update. /// See for more information. /// PostRequestedUpdate, - + /// /// An event that is fired before an addon calls its method. Refreshes are /// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to @@ -96,13 +96,13 @@ public enum AddonEvent /// /// PreRefresh, - + /// /// An event that is fired after an addon has finished its refresh. /// See for more information. /// PostRefresh, - + /// /// An event that is fired before an addon begins processing a user-driven event via /// , such as mousing over an element or clicking a button. This event @@ -112,10 +112,50 @@ public enum AddonEvent /// /// PreReceiveEvent, - + /// /// An event that is fired after an addon finishes calling its method. /// See for more information. /// PostReceiveEvent, + + /// + /// An event that is fired before an addon processes its open method. + /// + PreOpen, + + /// + /// An event that is fired after an addon has processed its open method. + /// + PostOpen, + + /// + /// An even that is fired before an addon processes its close method. + /// + PreClose, + + /// + /// An event that is fired after an addon has processed its close method. + /// + PostClose, + + /// + /// An event that is fired before an addon processes its show method. + /// + PreShow, + + /// + /// An event that is fired after an addon has processed its show method. + /// + PostShow, + + /// + /// An event that is fired before an addon processes its hide method. + /// + PreHide, + + /// + /// An event that is fired after an addon has processed its hide method. + /// + PostHide, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index b44ab8764..cea30d6be 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,16 +1,14 @@ using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Runtime.CompilerServices; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; -using Dalamud.Hooking.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle; @@ -26,69 +24,33 @@ internal unsafe class AddonLifecycle : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); + private readonly Dictionary modifiedTables = []; - private readonly nint disallowedReceiveEventAddress; - - private readonly AddonLifecycleAddressResolver address; - private readonly AddonSetupHook onAddonSetupHook; - private readonly Hook onAddonFinalizeHook; - private readonly CallHook onAddonDrawHook; - private readonly CallHook onAddonUpdateHook; - private readonly Hook onAddonRefreshHook; - private readonly CallHook onAddonRequestedUpdateHook; + private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) { - this.address = new AddonLifecycleAddressResolver(); - this.address.Setup(sigScanner); + this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); + this.onInitializeAddonHook.Enable(); - this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent; - - var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon; - - this.onAddonSetupHook = new AddonSetupHook(this.address.AddonSetup, this.OnAddonSetup); - this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); - this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); - this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); - this.onAddonRefreshHook = Hook.FromAddress(refreshAddonAddress, this.OnAddonRefresh); - this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); - - this.onAddonSetupHook.Enable(); - this.onAddonFinalizeHook.Enable(); - this.onAddonDrawHook.Enable(); - this.onAddonUpdateHook.Enable(); - this.onAddonRefreshHook.Enable(); - this.onAddonRequestedUpdateHook.Enable(); + Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}"); } - private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); - - /// - /// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks. - /// - internal List ReceiveEventListeners { get; } = new(); - /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal List EventListeners { get; } = new(); + internal List EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() { - this.onAddonSetupHook.Dispose(); - this.onAddonFinalizeHook.Dispose(); - this.onAddonDrawHook.Dispose(); - this.onAddonUpdateHook.Dispose(); - this.onAddonRefreshHook.Dispose(); - this.onAddonRequestedUpdateHook.Dispose(); + this.onInitializeAddonHook?.Dispose(); + this.onInitializeAddonHook = null; - foreach (var receiveEventListener in this.ReceiveEventListeners) + foreach (var virtualTable in this.modifiedTables.Values) { - receiveEventListener.Dispose(); + virtualTable.Dispose(); } } @@ -101,16 +63,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.framework.RunOnTick(() => { this.EventListeners.Add(listener); - - // If we want receive event messages have an already active addon, enable the receive event hook. - // If the addon isn't active yet, we'll grab the hook when it sets up. - if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) - { - receiveEventListener.TryEnable(); - } - } }); } @@ -122,24 +74,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. listener.Removed = true; - + this.framework.RunOnTick(() => { this.EventListeners.Remove(listener); - - // If we are disabling an ReceiveEvent listener, check if we should disable the hook. - if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - // Get the ReceiveEvent Listener for this addon - if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) - { - // If there are no other listeners listening for this event, disable the hook. - if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent)) - { - receiveEventListener.Disable(); - } - } - } }); } @@ -160,7 +98,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener. if (listener.Removed) continue; - + // Match on string.empty for listeners that want events for all addons. if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) continue; @@ -176,201 +114,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } } - private void RegisterReceiveEventHook(AtkUnitBase* addon) + private void OnAddonInitialize(AtkUnitBase* addon) { - // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. - // Disallows hooking the core internal event handler. - var addonName = addon->NameString; - var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent; - if (receiveEventAddress != this.disallowedReceiveEventAddress) + try { - // If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler. - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener) + this.LogInitialize(addon->NameString); + + if (!this.modifiedTables.ContainsKey(addon->NameString)) { - if (!existingListener.AddonNames.Contains(addonName)) + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + var managedVirtualTableEntry = new AddonVirtualTable(addon, this) { - existingListener.AddonNames.Add(addonName); - } - } + // This event is invoked when the game itself has disposed of an addon + // We can use this to know when to remove our virtual table entry + OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString), + }; - // Else, we have an addon that we don't have the ReceiveEvent for yet, make it. - else - { - this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress)); - } - - // If we have an active listener for this addon already, we need to activate this hook. - if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName)) - { - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener) - { - receiveEventListener.TryEnable(); - } + this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry); } } + catch (Exception e) + { + Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize."); + } + + this.onInitializeAddonHook!.Original(addon); } - private void UnregisterReceiveEventHook(string addonName) + [Conditional("DEBUG")] + private void LogInitialize(string addonName) { - // Remove this addons ReceiveEvent Registration - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener) - { - eventListener.AddonNames.Remove(addonName); - - // If there are no more listeners let's remove and dispose. - if (eventListener.AddonNames.Count is 0) - { - this.ReceiveEventListeners.Remove(eventListener); - eventListener.Dispose(); - } - } - } - - private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) - { - try - { - this.RegisterReceiveEventHook(addon); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); - } - - using var returner = this.argsPool.Rent(out AddonSetupArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkValueCount = valueCount; - arg.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreSetup, arg); - valueCount = arg.AtkValueCount; - values = (AtkValue*)arg.AtkValues; - - try - { - addon->OnSetup(valueCount, values); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostSetup, arg); - } - - private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) - { - try - { - var addonName = atkUnitBase[0]->NameString; - this.UnregisterReceiveEventHook(addonName); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); - } - - using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg); - arg.Clear(); - arg.Addon = (nint)atkUnitBase[0]; - this.InvokeListenersSafely(AddonEvent.PreFinalize, arg); - - try - { - this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); - } - } - - private void OnAddonDraw(AtkUnitBase* addon) - { - using var returner = this.argsPool.Rent(out AddonDrawArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - this.InvokeListenersSafely(AddonEvent.PreDraw, arg); - - try - { - addon->Draw(); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostDraw, arg); - } - - private void OnAddonUpdate(AtkUnitBase* addon, float delta) - { - using var returner = this.argsPool.Rent(out AddonUpdateArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.TimeDeltaInternal = delta; - this.InvokeListenersSafely(AddonEvent.PreUpdate, arg); - - try - { - addon->Update(delta); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostUpdate, arg); - } - - private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values) - { - var result = false; - - using var returner = this.argsPool.Rent(out AddonRefreshArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkValueCount = valueCount; - arg.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreRefresh, arg); - valueCount = arg.AtkValueCount; - values = (AtkValue*)arg.AtkValues; - - try - { - result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostRefresh, arg); - return result; - } - - private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) - { - using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.NumberArrayData = (nint)numberArrayData; - arg.StringArrayData = (nint)stringArrayData; - this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg); - numberArrayData = (NumberArrayData**)arg.NumberArrayData; - stringArrayData = (StringArrayData**)arg.StringArrayData; - - try - { - addon->OnRequestedUpdate(numberArrayData, stringArrayData); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg); + Log.Debug($"Initializing {addonName}"); } } @@ -387,7 +161,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); - private readonly List eventListeners = new(); + private readonly List eventListeners = []; /// void IInternalDisposableService.DisposeService() @@ -458,7 +232,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi this.eventListeners.RemoveAll(entry => { if (entry.FunctionDelegate != handler) return false; - + this.addonLifecycleService.UnregisterListener(entry); return true; }); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 854d666fd..1d767aac4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -1,56 +1,24 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Utility; namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver +[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")] +internal class AddonLifecycleAddressResolver : BaseAddressResolver { - /// - /// Gets the address of the addon setup hook invoked by the AtkUnitManager. - /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue. - /// This is called for a majority of all addon OnSetup's. - /// - public nint AddonSetup { get; private set; } - - /// - /// Gets the address of the other addon setup hook invoked by the AtkUnitManager. - /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue. - /// This seems to be called rarely for specific addons. - /// - public nint AddonSetup2 { get; private set; } - /// /// Gets the address of the addon finalize hook invoked by the AtkUnitManager. /// public nint AddonFinalize { get; private set; } - /// - /// Gets the address of the addon draw hook invoked by virtual function call. - /// - public nint AddonDraw { get; private set; } - - /// - /// Gets the address of the addon update hook invoked by virtual function call. - /// - public nint AddonUpdate { get; private set; } - - /// - /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. - /// - public nint AddonOnRequestedUpdate { get; private set; } - /// /// Scan for and setup any configured address pointers. /// /// The signature scanner to facilitate setup. protected override void Setup64Bit(ISigScanner sig) { - this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); - this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01"); - this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2"); - this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30"); } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs deleted file mode 100644 index 0d2bcc7f2..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Collections.Generic; - -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; -using Dalamud.Hooking; -using Dalamud.Logging.Internal; - -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent. -/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly. -/// -internal unsafe class AddonLifecycleReceiveEventListener : IDisposable -{ - private static readonly ModuleLog Log = new("AddonLifecycle"); - - [ServiceManager.ServiceDependency] - private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); - - /// - /// Initializes a new instance of the class. - /// - /// AddonLifecycle service instance. - /// Initial Addon Requesting this listener. - /// Address of Addon's ReceiveEvent function. - internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress) - { - this.AddonLifecycle = service; - this.AddonNames = [addonName]; - this.FunctionAddress = receiveEventAddress; - } - - /// - /// Gets the list of addons that use this receive event hook. - /// - public List AddonNames { get; init; } - - /// - /// Gets the address of the ReceiveEvent function as provided by the vtable on setup. - /// - public nint FunctionAddress { get; init; } - - /// - /// Gets the contained hook for these addons. - /// - public Hook? Hook { get; private set; } - - /// - /// Gets or sets the Reference to AddonLifecycle service instance. - /// - private AddonLifecycle AddonLifecycle { get; set; } - - /// - /// Try to hook and enable this receive event handler. - /// - public void TryEnable() - { - this.Hook ??= Hook.FromAddress(this.FunctionAddress, this.OnReceiveEvent); - this.Hook?.Enable(); - } - - /// - /// Disable the hook for this receive event handler. - /// - public void Disable() - { - this.Hook?.Disable(); - } - - /// - public void Dispose() - { - this.Hook?.Dispose(); - } - - private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) - { - // Check that we didn't get here through a call to another addons handler. - var addonName = addon->NameString; - if (!this.AddonNames.Contains(addonName)) - { - this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData); - return; - } - - using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkEventType = (byte)eventType; - arg.EventParam = eventParam; - arg.AtkEvent = (IntPtr)atkEvent; - arg.Data = (nint)atkEventData; - this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg); - eventType = (AtkEventType)arg.AtkEventType; - eventParam = arg.EventParam; - atkEvent = (AtkEvent*)arg.AtkEvent; - atkEventData = (AtkEventData*)arg.Data; - - try - { - this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); - } - - this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg); - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs deleted file mode 100644 index 297323b8f..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Runtime.InteropServices; - -using Reloaded.Hooks.Definitions; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// This class represents a callsite hook used to replace the address of the OnSetup function in r9. -/// -/// Delegate signature for this hook. -internal class AddonSetupHook : IDisposable where T : Delegate -{ - private readonly Reloaded.Hooks.AsmHook asmHook; - - private T? detour; - private bool activated; - - /// - /// Initializes a new instance of the class. - /// - /// Address of the instruction to replace. - /// Delegate to invoke. - internal AddonSetupHook(nint address, T detour) - { - this.detour = detour; - - var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); - var code = new[] - { - "use64", - $"mov r9, 0x{detourPtr:X8}", - }; - - var opt = new AsmHookOptions - { - PreferRelativeJump = true, - Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, - MaxOpcodeSize = 5, - }; - - this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); - } - - /// - /// Gets a value indicating whether the hook is enabled. - /// - public bool IsEnabled => this.asmHook.IsEnabled; - - /// - /// Starts intercepting a call to the function. - /// - public void Enable() - { - if (!this.activated) - { - this.activated = true; - this.asmHook.Activate(); - return; - } - - this.asmHook.Enable(); - } - - /// - /// Stops intercepting a call to the function. - /// - public void Disable() - { - this.asmHook.Disable(); - } - - /// - /// Remove a hook from the current process. - /// - public void Dispose() - { - this.asmHook.Disable(); - this.detour = null; - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs new file mode 100644 index 000000000..58e32a252 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -0,0 +1,405 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Logging.Internal; + +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon.Lifecycle; + +/// +/// Represents a class that holds references to an addons original and modified virtual table entries. +/// +internal unsafe class AddonVirtualTable : IDisposable +{ + // This need to be at minimum the largest virtual table size of all addons + // Copying extra entries is not problematic, and is considered safe. + private const int VirtualTableEntryCount = 200; + + private const bool EnableAdvancedLogging = true; + private const bool EnableSpammyLogging = false; + + private static readonly ModuleLog Log = new("LifecycleVT"); + + private readonly AddonLifecycle lifecycleService; + + // Obsolete warning is only to prevent users from creating their own event objects. +#pragma warning disable CS0618 // Type or member is obsolete + private readonly AddonSetupArgs addonSetupArg = new(); + private readonly AddonFinalizeArgs addonFinalizeArg = new(); + private readonly AddonDrawArgs addonDrawArg = new(); + private readonly AddonUpdateArgs addonUpdateArg = new(); + private readonly AddonRefreshArgs addonRefreshArg = new(); + private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); + private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); + private readonly AddonGenericArgs addonGenericArg = new(); +#pragma warning restore CS0618 // Type or member is obsolete + + private readonly AtkUnitBase* atkUnitBase; + + private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable; + private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable; + + // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table, + // the CLR needs to know they are in use, or it will invalidate them causing random crashing. + private readonly AtkUnitBase.Delegates.Dtor destructorFunction; + private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction; + private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction; + private readonly AtkUnitBase.Delegates.Draw drawFunction; + private readonly AtkUnitBase.Delegates.Update updateFunction; + private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction; + private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction; + private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction; + private readonly AtkUnitBase.Delegates.Open openFunction; + private readonly AtkUnitBase.Delegates.Close closeFunction; + private readonly AtkUnitBase.Delegates.Show showFunction; + private readonly AtkUnitBase.Delegates.Hide hideFunction; + + /// + /// Initializes a new instance of the class. + /// + /// AtkUnitBase* for the addon to replace the table of. + /// Reference to AddonLifecycle service to callback and invoke listeners. + internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService) + { + this.atkUnitBase = addon; + this.lifecycleService = lifecycleService; + + // Save original virtual table + this.originalVirtualTable = addon->VirtualTable; + + // Create copy of original table + // Note this will copy any derived/overriden functions that this specific addon has. + // Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game + this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8); + NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + + // Overwrite the addons existing virtual table with our own + addon->VirtualTable = this.modifiedVirtualTable; + + // Pin each of our listener functions + this.destructorFunction = this.OnAddonDestructor; + this.onSetupFunction = this.OnAddonSetup; + this.finalizerFunction = this.OnAddonFinalize; + this.drawFunction = this.OnAddonDraw; + this.updateFunction = this.OnAddonUpdate; + this.onRefreshFunction = this.OnAddonRefresh; + this.onRequestedUpdateFunction = this.OnRequestedUpdate; + this.onReceiveEventFunction = this.OnAddonReceiveEvent; + this.openFunction = this.OnAddonOpen; + this.closeFunction = this.OnAddonClose; + this.showFunction = this.OnAddonShow; + this.hideFunction = this.OnAddonHide; + + // Overwrite specific virtual table entries + this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); + this.modifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction); + this.modifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction); + this.modifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction); + this.modifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); + this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction); + this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction); + this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction); + this.modifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction); + this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); + this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); + this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); + } + + /// + /// Gets an event that is invoked when this addon's Finalize method is called from native. + /// + public required Action OnAddonFinalized { get; init; } + + /// + /// WARNING! This should not be called at any time except during dalamud unload. + /// + public void Dispose() + { + this.atkUnitBase->VirtualTable = this.originalVirtualTable; + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + } + + private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) + { + this.LogEvent(); + + var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); + + if ((freeFlags & 1) == 1) + { + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + this.OnAddonFinalized(); + } + + return result; + } + + private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + this.LogEvent(); + + this.addonSetupArg.Clear(); + this.addonSetupArg.Addon = addon; + this.addonSetupArg.AtkValueCount = valueCount; + this.addonSetupArg.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg); + valueCount = this.addonSetupArg.AtkValueCount; + values = (AtkValue*)this.addonSetupArg.AtkValues; + + try + { + this.originalVirtualTable->OnSetup(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg); + } + + private void OnAddonFinalize(AtkUnitBase* thisPtr) + { + this.LogEvent(); + + this.addonFinalizeArg.Clear(); + this.addonFinalizeArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg); + + try + { + this.originalVirtualTable->Finalizer(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); + } + } + + private void OnAddonDraw(AtkUnitBase* addon) + { + this.LogEvent(); + + this.addonDrawArg.Clear(); + this.addonDrawArg.Addon = addon; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); + + try + { + this.originalVirtualTable->Draw(addon); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg); + } + + private void OnAddonUpdate(AtkUnitBase* addon, float delta) + { + this.LogEvent(); + + this.addonUpdateArg.Clear(); + this.addonUpdateArg.Addon = addon; + this.addonUpdateArg.TimeDeltaInternal = delta; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); + + try + { + this.originalVirtualTable->Update(addon, delta); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg); + } + + private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + this.LogEvent(); + + var result = false; + + this.addonRefreshArg.Clear(); + this.addonRefreshArg.Addon = addon; + this.addonRefreshArg.AtkValueCount = valueCount; + this.addonRefreshArg.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg); + valueCount = this.addonRefreshArg.AtkValueCount; + values = (AtkValue*)this.addonRefreshArg.AtkValues; + + try + { + result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg); + return result; + } + + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + this.LogEvent(); + + this.addonRequestedUpdateArg.Clear(); + this.addonRequestedUpdateArg.Addon = addon; + this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; + this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg); + numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData; + stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData; + + try + { + this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg); + } + + private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) + { + this.LogEvent(); + + this.addonReceiveEventArg.Clear(); + this.addonReceiveEventArg.Addon = (nint)addon; + this.addonReceiveEventArg.AtkEventType = (byte)eventType; + this.addonReceiveEventArg.EventParam = eventParam; + this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent; + this.addonReceiveEventArg.Data = (nint)atkEventData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg); + eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType; + eventParam = this.addonReceiveEventArg.EventParam; + atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent; + atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data; + + try + { + this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg); + } + + private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) + { + this.LogEvent(); + + var result = false; + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); + + try + { + result = this.originalVirtualTable->Open(thisPtr, depthLayer); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg); + + return result; + } + + private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) + { + this.LogEvent(); + + var result = false; + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); + + try + { + result = this.originalVirtualTable->Close(thisPtr, fireCallback); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg); + + return result; + } + + private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) + { + this.LogEvent(); + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); + + try + { + this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg); + } + + private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) + { + this.LogEvent(); + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); + + try + { + this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg); + } + + [Conditional("DEBUG")] + private void LogEvent([CallerMemberName] string caller = "") + { + if (EnableAdvancedLogging) + { + if (!EnableSpammyLogging) + { + if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") + return; + } + + Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}"); + } + } +} diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs deleted file mode 100644 index 92bc6e31a..000000000 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Runtime.InteropServices; - -using Reloaded.Hooks.Definitions; - -namespace Dalamud.Hooking.Internal; - -/// -/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook. -/// This is a destructive operation, no other callsite hooks can coexist at the same address. -/// -/// There's no .Original for this hook type. -/// This is only intended for be for functions where the parameters provided allow you to invoke the original call. -/// -/// This class was specifically added for hooking virtual function callsites. -/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered. -/// -/// Delegate signature for this hook. -internal class CallHook : IDalamudHook where T : Delegate -{ - private readonly Reloaded.Hooks.AsmHook asmHook; - - private T? detour; - private bool activated; - - /// - /// Initializes a new instance of the class. - /// - /// Address of the instruction to replace. - /// Delegate to invoke. - internal CallHook(nint address, T detour) - { - ArgumentNullException.ThrowIfNull(detour); - - this.detour = detour; - this.Address = address; - - var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); - var code = new[] - { - "use64", - $"mov rax, 0x{detourPtr:X8}", - "call rax", - }; - - var opt = new AsmHookOptions - { - PreferRelativeJump = true, - Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, - MaxOpcodeSize = 5, - }; - - this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); - } - - /// - /// Gets a value indicating whether the hook is enabled. - /// - public bool IsEnabled => this.asmHook.IsEnabled; - - /// - public IntPtr Address { get; } - - /// - public string BackendName => "Reloaded AsmHook"; - - /// - public bool IsDisposed => this.detour == null; - - /// - /// Starts intercepting a call to the function. - /// - public void Enable() - { - if (!this.activated) - { - this.activated = true; - this.asmHook.Activate(); - return; - } - - this.asmHook.Enable(); - } - - /// - /// Stops intercepting a call to the function. - /// - public void Disable() - { - this.asmHook.Disable(); - } - - /// - /// Remove a hook from the current process. - /// - public void Dispose() - { - this.asmHook.Disable(); - this.detour = null; - } -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index b58166e89..c336f895e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -54,13 +52,6 @@ public class AddonLifecycleWidget : IDataWindowWidget this.DrawEventListeners(); ImGui.Unindent(); } - - if (ImGui.CollapsingHeader("ReceiveEvent Hooks"u8)) - { - ImGui.Indent(); - this.DrawReceiveEventHooks(); - ImGui.Unindent(); - } } private void DrawEventListeners() @@ -100,46 +91,4 @@ public class AddonLifecycleWidget : IDataWindowWidget } } } - - private void DrawReceiveEventHooks() - { - if (!this.Ready) return; - - var listeners = this.AddonLifecycle.ReceiveEventListeners; - - if (listeners.Count == 0) - { - ImGui.Text("No ReceiveEvent Hooks are Registered"u8); - } - - foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners) - { - if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames))) - { - ImGui.Columns(2); - - var functionAddress = receiveEventListener.FunctionAddress; - - ImGui.Text("Hook Address"u8); - ImGui.NextColumn(); - ImGui.Text($"0x{functionAddress:X} (ffxiv_dx11.exe+{functionAddress - Process.GetCurrentProcess().MainModule!.BaseAddress:X})"); - - ImGui.NextColumn(); - ImGui.Text("Hook Status"u8); - ImGui.NextColumn(); - if (receiveEventListener.Hook is null) - { - ImGui.Text("Hook is null"u8); - } - else - { - var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed; - var text = receiveEventListener.Hook.IsEnabled ? "Enabled"u8 : "Disabled"u8; - ImGui.TextColored(color, text); - } - - ImGui.Columns(1); - } - } - } } From 2c1bb7664331d975c9398342b89cba88651df0b5 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 18:56:34 -0800 Subject: [PATCH 203/477] Minor cleanup --- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 11 ++++++----- .../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 95dc5f718..de32bd254 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -41,7 +41,7 @@ public enum AddonArgsType ReceiveEvent, /// - /// Generic arg type that contains no meaningful data + /// Generic arg type that contains no meaningful data. /// Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cea30d6be..0c23f5661 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -33,8 +33,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); this.onInitializeAddonHook.Enable(); - - Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}"); } /// @@ -48,10 +46,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - foreach (var virtualTable in this.modifiedTables.Values) + this.framework.RunOnFrameworkThread(() => { - virtualTable.Dispose(); - } + foreach (var virtualTable in this.modifiedTables.Values) + { + virtualTable.Dispose(); + } + }); } /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 1d767aac4..9359870a5 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")] +[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 58e32a252..ca5d970ef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,7 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = true; + private const bool EnableAdvancedLogging = false; private const bool EnableSpammyLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); From ab0500ca6f9ff49cf0c48da7c585ae8e7429fbdf Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 20:45:54 -0800 Subject: [PATCH 204/477] Fix unreachable code complaint --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index ca5d970ef..54c91248e 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,8 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = false; - private const bool EnableSpammyLogging = false; + private const bool EnableLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); @@ -125,7 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); @@ -140,7 +139,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; @@ -164,7 +163,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonFinalize(AtkUnitBase* thisPtr) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; @@ -182,7 +181,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonDraw(AtkUnitBase* addon) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; @@ -202,7 +201,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; @@ -223,7 +222,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -250,7 +249,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; @@ -274,7 +273,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; @@ -302,7 +301,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -326,7 +325,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -350,7 +349,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -370,7 +369,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -389,15 +388,13 @@ internal unsafe class AddonVirtualTable : IDisposable } [Conditional("DEBUG")] - private void LogEvent([CallerMemberName] string caller = "") + private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") { - if (EnableAdvancedLogging) + if (loggingEnabled) { - if (!EnableSpammyLogging) - { - if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") - return; - } + // Manually disable the really spammy log events, you can comment this out if you need to debug them. + if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") + return; Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}"); } From efd66fd3f87b5b5a452cdc47b304a2a35eaae99f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 26 Nov 2025 01:43:01 +0100 Subject: [PATCH 205/477] Handle errors in Window draw events by displaying an error message --- Dalamud/Interface/Windowing/Window.cs | 71 ++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index d4f0070db..44ff62199 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -55,6 +55,9 @@ public abstract class Window private Vector2 fadeOutSize = Vector2.Zero; private Vector2 fadeOutOrigin = Vector2.Zero; + private bool hasError = false; + private Exception? lastError; + /// /// Initializes a new instance of the class. /// @@ -458,14 +461,24 @@ public abstract class Window this.presetDirty = true; } - // Draw the actual window contents - try + if (this.hasError) { - this.Draw(); + this.DrawErrorMessage(); } - catch (Exception ex) + else { - Log.Error(ex, "Error during Draw(): {WindowName}", this.WindowName); + // Draw the actual window contents + try + { + this.Draw(); + } + catch (Exception ex) + { + Log.Error(ex, "Error during Draw(): {WindowName}", this.WindowName); + + this.hasError = true; + this.lastError = ex; + } } } @@ -793,7 +806,7 @@ public abstract class Window hovered = true; // 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) + if ((Windows.Win32.PInvoke.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0) { held = true; pressed = true; @@ -871,6 +884,52 @@ public abstract class Window ImGui.End(); } + private void DrawErrorMessage() + { + // TODO: Once window systems are services, offer to reload the plugin + ImGui.TextColoredWrapped(ImGuiColors.DalamudRed,Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); + + ImGuiHelpers.ScaledDummy(5); + + if (ImGui.Button(Loc.Localize("WindowSystemErrorRecoverButton", "Attempt to retry"))) + { + this.hasError = false; + this.lastError = null; + } + + ImGui.SameLine(); + + if (ImGui.Button(Loc.Localize("WindowSystemErrorClose", "Close Window"))) + { + this.IsOpen = false; + this.hasError = false; + this.lastError = null; + } + + ImGuiHelpers.ScaledDummy(10); + + if (this.lastError != null) + { + using var child = ImRaii.Child("##ErrorDetails", new Vector2(0, 200 * ImGuiHelpers.GlobalScale), true); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) + { + ImGui.TextWrapped(Loc.Localize("WindowSystemErrorDetails", "Error Details:")); + ImGui.Separator(); + ImGui.TextWrapped(this.lastError.ToString()); + } + + var childWindowSize = ImGui.GetWindowSize(); + var copyText = Loc.Localize("WindowSystemErrorCopy", "Copy"); + var buttonWidth = ImGuiComponents.GetIconButtonWithTextWidth(FontAwesomeIcon.Copy, copyText); + ImGui.SetCursorPos(new Vector2(childWindowSize.X - buttonWidth - ImGui.GetStyle().FramePadding.X, + ImGui.GetStyle().FramePadding.Y)); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Copy, copyText)) + { + ImGui.SetClipboardText(this.lastError.ToString()); + } + } + } + /// /// Structure detailing the size constraints of a window. /// From 2cef75bbbef1862f85eb582c0b732c58c6b4a135 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 26 Nov 2025 11:56:30 -0800 Subject: [PATCH 206/477] feat: remove socket cleanup tasks --- .../Rpc/Transport/UnixRpcTransport.cs | 9 -- Dalamud/Networking/Rpc/UnixSocketUtil.cs | 92 ------------------- 2 files changed, 101 deletions(-) delete mode 100644 Dalamud/Networking/Rpc/UnixSocketUtil.cs diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs index 064ce375d..17da51444 100644 --- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -8,8 +8,6 @@ using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Utility; -using TerraFX.Interop.Windows; - namespace Dalamud.Networking.Rpc.Transport; /// @@ -94,13 +92,6 @@ internal class UnixRpcTransport : IRpcTransport } this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); - - // note: needs to be run _after_ we're alive so that we don't delete our own socket. - // TODO: This should *probably* be handed by the launcher instead. - if (this.cleanupSocketDirectory != null) - { - Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory)); - } } /// Invoke an RPC request on a specific client expecting a result. diff --git a/Dalamud/Networking/Rpc/UnixSocketUtil.cs b/Dalamud/Networking/Rpc/UnixSocketUtil.cs deleted file mode 100644 index b7500a946..000000000 --- a/Dalamud/Networking/Rpc/UnixSocketUtil.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.IO; -using System.Net.Sockets; -using System.Threading.Tasks; - -using Serilog; - -namespace Dalamud.Networking.Rpc; - -/// -/// A set of utilities to help manage Unix sockets. -/// -internal static class UnixSocketUtil -{ - // Default probe timeout in milliseconds. - private const int DefaultProbeMs = 200; - - /// - /// Test whether a Unix socket is alive/listening. - /// - /// The path to test. - /// How long to wait for a connection success. - /// A task result representing if a socket is alive or not. - public static async Task IsSocketAlive(string path, int timeoutMs = DefaultProbeMs) - { - if (string.IsNullOrEmpty(path)) return false; - var endpoint = new UnixDomainSocketEndPoint(path); - using var client = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - - var connectTask = client.ConnectAsync(endpoint); - var completed = await Task.WhenAny(connectTask, Task.Delay(timeoutMs)).ConfigureAwait(false); - - if (completed == connectTask) - { - // Connected or failed very quickly. If the task is successful, the socket is alive. - if (connectTask.IsCompletedSuccessfully) - { - try - { - client.Shutdown(SocketShutdown.Both); - } - catch - { - // ignored - } - - return true; - } - } - - return false; - } - - /// - /// Find and remove stale Dalamud RPC sockets. - /// - /// The directory to scan for stale sockets. - /// The timeout to wait for a connection attempt to succeed. - /// A task that executes when sockets are purged. - public static async Task CleanStaleSockets(string directory, int probeTimeoutMs = DefaultProbeMs) - { - if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return; - - foreach (var file in Directory.EnumerateFiles(directory, "DalamudRPC.*.sock", SearchOption.TopDirectoryOnly)) - { - // we don't need to check ourselves. - if (file.Contains(Environment.ProcessId.ToString())) continue; - - bool shouldDelete; - - try - { - shouldDelete = !await IsSocketAlive(file, probeTimeoutMs); - } - catch - { - shouldDelete = true; - } - - if (shouldDelete) - { - try - { - File.Delete(file); - } - catch (Exception ex) - { - Log.Error(ex, "Could not delete stale socket file: {File}", file); - } - } - } - } -} From 9e5195492eb34171b6751c017ee67557ec181a1d Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 26 Nov 2025 21:07:49 +0100 Subject: [PATCH 207/477] Get active track from env var, instead of git branch --- Dalamud/Interface/Internal/DalamudInterface.cs | 2 +- .../Internal/Windows/BranchSwitcherWindow.cs | 10 ++++++---- Dalamud/Utility/Util.cs | 16 +++++++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index f2ffc7a4c..202334580 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1060,7 +1060,7 @@ internal class DalamudInterface : IInternalDisposableService { ImGui.PushFont(InterfaceManager.MonoFont); - ImGui.BeginMenu(Util.GetBranch() ?? "???", false); + ImGui.BeginMenu($"{Util.GetActiveTrack() ?? "???"} on {Util.GetGitBranch() ?? "???"}", false); ImGui.BeginMenu($"{Util.GetScmVersion()}", false); ImGui.BeginMenu(this.FrameCount.ToString("000000"), false); ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index da6217aca..51ff3bdcd 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -46,10 +46,12 @@ public class BranchSwitcherWindow : Window this.branches = await client.GetFromJsonAsync>(BranchInfoUrl); Debug.Assert(this.branches != null, "this.branches != null"); - var branch = Util.GetBranch(); - this.selectedBranchIndex = this.branches!.Any(x => x.Value.Track == branch) ? - this.branches.TakeWhile(x => x.Value.Track != branch).Count() - : 0; + var trackName = Util.GetActiveTrack(); + this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track != trackName); + if (this.selectedBranchIndex == -1) + { + this.selectedBranchIndex = 0; + } }); base.OnOpen(); diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 2a3733303..19610ef64 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -140,10 +140,10 @@ public static partial class Util } /// - /// Gets the Dalamud branch name this version of Dalamud was built from, or null, if this is a Debug build. + /// Gets the Git branch name this version of Dalamud was built from, or null, if this is a Debug build. /// /// The branch name. - public static string? GetBranch() + public static string? GetGitBranch() { if (branchInternal != null) return branchInternal; @@ -155,7 +155,17 @@ public static partial class Util if (gitBranch == null) return null; - return branchInternal = gitBranch == "master" ? "release" : gitBranch; + return branchInternal = gitBranch; + } + + /// + /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version + /// downloaded from webservices. + /// + /// The name of the track, or null. + internal static string? GetActiveTrack() + { + return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); } /// From c6b173dd63b89f13c1621a08557261bf0ebcae9a Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 26 Nov 2025 21:22:59 +0100 Subject: [PATCH 208/477] build: 13.0.0.11 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index b5062438a..ce140b8c9 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.10 + 13.0.0.11 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From c136934aa838fd1376e874117f39df0ce6702a61 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 26 Nov 2025 21:46:07 +0100 Subject: [PATCH 209/477] Always pass a key, even for release Fixes an issue wherein the XL commandline parser wouldn't like the empty argument and error out --- Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index 51ff3bdcd..4e95b718e 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -105,7 +105,7 @@ public class BranchSwitcherWindow : Window }; ps.ArgumentList.Add($"--dalamud-beta-kind={config.DalamudBetaKind}"); - ps.ArgumentList.Add($"--dalamud-beta-key={config.DalamudBetaKey}"); + ps.ArgumentList.Add($"--dalamud-beta-key={(config.DalamudBetaKey.IsNullOrEmpty() ? "invalid" : config.DalamudBetaKey)}"); Process.Start(ps); Environment.Exit(0); From 4c3ba35f07a0fe9bf2a151f6734bdfbe7a4179db Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 27 Nov 2025 01:45:13 +0100 Subject: [PATCH 210/477] Don't inhibit ATK close events if pinned or clickthrough windows are focused --- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 4 ++-- Dalamud/Interface/Internal/InterfaceManager.cs | 1 + Dalamud/Interface/Windowing/Window.cs | 10 ++++++++++ Dalamud/Interface/Windowing/WindowSystem.cs | 13 ++++++++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 486af463c..466401ef3 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -113,7 +113,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService private void AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { // 3 == Close - if (eventType == AtkEventType.InputReceived && WindowSystem.HasAnyWindowSystemFocus && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled) + if (eventType == AtkEventType.InputReceived && WindowSystem.ShouldInhibitAtkCloseEvents && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled) { Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}"); return; @@ -124,7 +124,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService private void AgentHudOpenSystemMenuDetour(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize) { - if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled) + if (WindowSystem.ShouldInhibitAtkCloseEvents && this.configuration.IsFocusManagementEnabled) { Log.Verbose($"Cancelling OpenSystemMenu due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}"); return; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d68bc8bef..76a1b5172 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1175,6 +1175,7 @@ internal partial class InterfaceManager : IInternalDisposableService WindowSystem.HasAnyWindowSystemFocus = false; WindowSystem.FocusedWindowSystemNamespace = string.Empty; + WindowSystem.ShouldInhibitAtkCloseEvents = false; if (this.IsDispatchingEvents) { diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 44ff62199..f12e87099 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -226,6 +226,16 @@ public abstract class Window /// public bool AllowClickthrough { get; set; } = true; + /// + /// Gets a value indicating whether this window is pinned. + /// + public bool IsPinned => this.internalIsPinned; + + /// + /// Gets a value indicating whether this window is click-through. + /// + public bool IsClickthrough => this.internalIsClickthrough; + /// /// Gets or sets a list of available title bar buttons. /// diff --git a/Dalamud/Interface/Windowing/WindowSystem.cs b/Dalamud/Interface/Windowing/WindowSystem.cs index 87bd199a1..d6e9649bb 100644 --- a/Dalamud/Interface/Windowing/WindowSystem.cs +++ b/Dalamud/Interface/Windowing/WindowSystem.cs @@ -60,6 +60,12 @@ public class WindowSystem /// public string? Namespace { get; set; } + /// + /// Gets or sets a value indicating whether ATK close events should be inhibited while any window has focus. + /// Does not respect windows that are pinned or clickthrough. + /// + internal static bool ShouldInhibitAtkCloseEvents { get; set; } + /// /// Add a window to this . /// The window system doesn't own your window, it just renders it @@ -130,7 +136,7 @@ public class WindowSystem window.DrawInternal(flags, persistence); } - var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused && window.RespectCloseHotkey); + var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused); this.HasAnyFocus = focusedWindow != default; if (this.HasAnyFocus) @@ -155,6 +161,11 @@ public class WindowSystem } } + ShouldInhibitAtkCloseEvents |= this.windows.Any(w => w.IsFocused && + w.RespectCloseHotkey && + !w.IsPinned && + !w.IsClickthrough); + if (hasNamespace) ImGui.PopID(); } From c661faea6be8142a9005431ddaccf88ac2ce9025 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 12 Nov 2025 21:49:28 +0100 Subject: [PATCH 211/477] Fix services using wrong namespaces --- .../Game/Addon/Events/AddonEventManagerAddressResolver.cs | 4 +++- .../Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +- Dalamud/Game/BaseAddressResolver.cs | 2 ++ Dalamud/Game/ClientState/ClientStateAddressResolver.cs | 2 ++ Dalamud/Game/ClientState/Objects/TargetManager.cs | 1 + Dalamud/Game/Config/GameConfigAddressResolver.cs | 4 +++- Dalamud/Game/DutyState/DutyStateAddressResolver.cs | 2 ++ Dalamud/Game/Gui/GameGuiAddressResolver.cs | 2 ++ Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs | 2 ++ Dalamud/Game/Network/GameNetworkAddressResolver.cs | 2 ++ .../Game/Network/Internal/NetworkHandlersAddressResolver.cs | 4 +++- Dalamud/Game/SigScanner.cs | 2 ++ Dalamud/Game/TargetSigScanner.cs | 3 ++- Dalamud/Plugin/{SelfTest => Services}/ISelfTestRegistry.cs | 4 ++-- Dalamud/Plugin/Services/ISigScanner.cs | 4 +--- Dalamud/Plugin/Services/ITargetManager.cs | 6 +++--- 16 files changed, 33 insertions(+), 13 deletions(-) rename Dalamud/Plugin/{SelfTest => Services}/ISelfTestRegistry.cs (95%) diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs index 415e1b169..ec1c51a12 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Addon.Events; +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Addon.Events; /// /// AddonEventManager memory address resolver. diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 854d666fd..bc9e4b639 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Plugin.Services; namespace Dalamud.Game.Addon.Lifecycle; diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index 4133117d7..d41b1d9d8 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using Dalamud.Plugin.Services; + namespace Dalamud.Game; /// diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 2fc859d09..53774121d 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.ClientState; /// diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index f81154693..a6432e242 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Control; diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs index 2491c4033..e03f4f40b 100644 --- a/Dalamud/Game/Config/GameConfigAddressResolver.cs +++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Config; +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Config; /// /// Game config system address resolver. diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs index 1bca93efb..480b699a0 100644 --- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs +++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.DutyState; /// diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs index 92b89c5a9..1295e2047 100644 --- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.Gui; /// diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs index 450e1fa9f..f97450c28 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.Gui.NamePlate; /// diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs index de92f7c10..48abc2d97 100644 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ b/Dalamud/Game/Network/GameNetworkAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.Network; /// diff --git a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs index 9cd46f798..34c071556 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Network.Internal; +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Network.Internal; /// /// Internal address resolver for the network handlers. diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index c8a371aee..262e98fa5 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -8,6 +8,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; +using Dalamud.Plugin.Services; + using Iced.Intel; using Newtonsoft.Json; using Serilog; diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index f60c32d9a..540d0ea47 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -1,8 +1,9 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; namespace Dalamud.Game; diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/Services/ISelfTestRegistry.cs similarity index 95% rename from Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs rename to Dalamud/Plugin/Services/ISelfTestRegistry.cs index 7e9faf3f9..50d3d35ce 100644 --- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs +++ b/Dalamud/Plugin/Services/ISelfTestRegistry.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.SelfTest; -namespace Dalamud.Plugin.SelfTest; +namespace Dalamud.Plugin.Services; /// /// Interface for registering and unregistering self-test steps from plugins. diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs index fbbd8b05a..017c4fe9d 100644 --- a/Dalamud/Plugin/Services/ISigScanner.cs +++ b/Dalamud/Plugin/Services/ISigScanner.cs @@ -2,9 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; -using Dalamud.Plugin.Services; - -namespace Dalamud.Game; +namespace Dalamud.Plugin.Services; /// /// A SigScanner facilitates searching for memory signatures in a given ProcessModule. diff --git a/Dalamud/Plugin/Services/ITargetManager.cs b/Dalamud/Plugin/Services/ITargetManager.cs index 9c9fce550..0c14571c5 100644 --- a/Dalamud/Plugin/Services/ITargetManager.cs +++ b/Dalamud/Plugin/Services/ITargetManager.cs @@ -1,7 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; -namespace Dalamud.Game.ClientState.Objects; +namespace Dalamud.Plugin.Services; /// /// Get and set various kinds of targets for the player. @@ -37,13 +37,13 @@ public interface ITargetManager : IDalamudService /// Set to null to clear the target. /// public IGameObject? SoftTarget { get; set; } - + /// /// Gets or sets the gpose target. /// Set to null to clear the target. /// public IGameObject? GPoseTarget { get; set; } - + /// /// Gets or sets the mouseover nameplate target. /// Set to null to clear the target. From c525655be66778cd3d092d32f6ae5aba16c0fbea Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:24:35 -0800 Subject: [PATCH 212/477] Improve LifecycleInvoke efficiency with Dictionary --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 0c23f5661..cf1270803 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal List EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,10 +61,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => - { - this.EventListeners.Add(listener); - }); + this.EventListeners.TryAdd(listener.EventType, [ listener ]); + this.EventListeners[listener.EventType].Add(listener); } /// @@ -73,13 +71,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. - listener.Removed = true; - - this.framework.RunOnTick(() => + if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) { - this.EventListeners.Remove(listener); - }); + listenerList.Remove(listener); + } } /// @@ -90,16 +85,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// What to blame on errors. internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { + // Early return if we don't have any listeners of this type + if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in this.EventListeners) + foreach (var listener in listenerList) { - if (listener.EventType != eventType) - continue; - - // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener. - if (listener.Removed) - continue; - // Match on string.empty for listeners that want events for all addons. if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) continue; From 166f249e13ed310db4bc44a658d8d473b92ae6a2 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:30:40 -0800 Subject: [PATCH 213/477] Use hashset to prevent duplicate entries --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cf1270803..403671920 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() From 2a60bc61a7713172bf60ff1b2b78076b44473ac4 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 15:52:18 -0800 Subject: [PATCH 214/477] Force style vars so erroring window renders at least partially sanely --- Dalamud/Interface/Windowing/Window.cs | 47 ++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index f12e87099..5169b9746 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -57,6 +57,7 @@ public abstract class Window private bool hasError = false; private Exception? lastError; + private bool isErrorStylePushed; /// /// Initializes a new instance of the class. @@ -425,8 +426,16 @@ public abstract class Window UIGlobals.PlaySoundEffect(this.OnOpenSfxId); } - this.PreDraw(); - this.ApplyConditionals(); + if (!this.hasError) + { + this.PreDraw(); + this.ApplyConditionals(); + } + else + { + Style.StyleModelV1.DalamudStandard.Push(); + this.isErrorStylePushed = true; + } if (this.ForceMainWindow) ImGuiHelpers.ForceNextWindowMainViewport(); @@ -448,10 +457,28 @@ public abstract class Window var flags = this.Flags; if (this.internalIsPinned || this.internalIsClickthrough) - flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; + { + if (!this.hasError) + { + flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; + } + else + { + flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize); + } + } if (this.internalIsClickthrough) - flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs; + { + if (!this.hasError) + { + flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs; + } + else + { + flags &= ~(ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs); + } + } if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags)) { @@ -670,7 +697,17 @@ public abstract class Window Task.FromResult(tex)); } - this.PostDraw(); + if (!this.hasError) + { + this.PostDraw(); + } + else + { + if (this.isErrorStylePushed) + { + Style.StyleModelV1.DalamudStandard.Pop(); + } + } this.PostHandlePreset(persistence); From 29c154f9b5a2d7ab1cabd41cbaac432d06c2b27d Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 08:35:54 -0800 Subject: [PATCH 215/477] Fix accidentally breaking widget --- .../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index c336f895e..73c4e540a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; @@ -58,12 +57,11 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var eventType in Enum.GetValues()) + foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(eventType.ToString())) + if (ImGui.CollapsingHeader(listenerType.ToString())) { ImGui.Indent(); - var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList(); if (listeners.Count == 0) { From 325d28ee3211d7743fc407f9f934e4bbc66ec48a Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:08:24 -0800 Subject: [PATCH 216/477] further improve performance --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 61 ++++++++++++++----- .../Data/Widgets/AddonLifecycleWidget.cs | 40 ++++++------ 2 files changed, 67 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 403671920..e38f56921 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + /// Mapping is: EventType -> AddonName -> ListenerList + internal Dictionary>> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,8 +62,18 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.EventListeners.TryAdd(listener.EventType, [ listener ]); - this.EventListeners[listener.EventType].Add(listener); + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + this.EventListeners.TryAdd(listener.EventType, []); + } + + // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) + { + this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []); + } + + this.EventListeners[listener.EventType][listener.AddonName].Add(listener); } /// @@ -71,9 +82,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) + if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) { - listenerList.Remove(listener); + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + { + addonListener.Remove(listener); + } } } @@ -86,22 +100,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { // Early return if we don't have any listeners of this type - if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; - // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in listenerList) + // Handle listeners for this event type that don't care which addon is triggering it + if (addonListeners.TryGetValue(string.Empty, out var globalListeners)) { - // Match on string.empty for listeners that want events for all addons. - if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) - continue; - - try + foreach (var listener in globalListeners) { - listener.FunctionDelegate.Invoke(eventType, args); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener."); + } } - catch (Exception e) + } + + // Handle listeners that are listening for this addon and event type specifically + if (addonListeners.TryGetValue(args.AddonName, out var addonListener)) + { + foreach (var listener in addonListener) { - Log.Error(e, $"Exception in {blame} during {eventType} invoke."); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}."); + } } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 73c4e540a..0f193556b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -2,7 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -57,35 +58,38 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) + foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(listenerType.ToString())) + using var eventId = ImRaii.PushId(eventType.ToString()); + + if (ImGui.CollapsingHeader(eventType.ToString())) { - ImGui.Indent(); + using var eventIndent = ImRaii.PushIndent(); - if (listeners.Count == 0) + if (addonListeners.Count == 0) { - ImGui.Text("No Listeners Registered for Event"u8); + ImGui.Text("No Addons Registered for Event"u8); } - if (ImGui.BeginTable("AddonLifecycleListenersTable"u8, 2)) + foreach (var (addonName, listeners) in addonListeners) { - ImGui.TableSetupColumn("##AddonName"u8, ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##MethodInvoke"u8, ImGuiTableColumnFlags.WidthStretch); + using var addonId = ImRaii.PushId(addonName); - foreach (var listener in listeners) + if (ImGui.CollapsingHeader(addonName.IsNullOrEmpty() ? "GLOBAL" : addonName)) { - ImGui.TableNextColumn(); - ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); + using var addonIndent = ImRaii.PushIndent(); - ImGui.TableNextColumn(); - ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + if (listeners.Count == 0) + { + ImGui.Text("No Listeners Registered for Event"u8); + } + + foreach (var listener in listeners) + { + ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + } } - - ImGui.EndTable(); } - - ImGui.Unindent(); } } } From 170f6e08599d4853acc260aa3a442171d30a1731 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:11:13 -0800 Subject: [PATCH 217/477] Remove redundant header --- .../Windows/Data/Widgets/AddonLifecycleWidget.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 0f193556b..4fb13b81a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -46,18 +46,6 @@ public class AddonLifecycleWidget : IDataWindowWidget return; } - if (ImGui.CollapsingHeader("Listeners"u8)) - { - ImGui.Indent(); - this.DrawEventListeners(); - ImGui.Unindent(); - } - } - - private void DrawEventListeners() - { - if (!this.Ready) return; - foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { using var eventId = ImRaii.PushId(eventType.ToString()); From d7915c7020f04f15eaa4caa2c341b2356c51a08f Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 28 Nov 2025 18:11:31 +0100 Subject: [PATCH 218/477] Show a sensible error message when Lumina fails to init --- Dalamud/Data/DataManager.cs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index d017bf85a..ed0aa6c4d 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -41,7 +41,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager try { Log.Verbose("Starting data load..."); - + using (Timings.Start("Lumina Init")) { var luminaOptions = new LuminaOptions @@ -53,12 +53,25 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager DefaultExcelLanguage = this.Language.ToLumina(), }; - this.GameData = new( - Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"), - luminaOptions) + try { - StreamPool = new(), - }; + this.GameData = new( + Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"), + luminaOptions) + { + StreamPool = new(), + }; + } + catch (Exception ex) + { + Log.Error(ex, "Lumina GameData init failed"); + Util.Fatal( + "Dalamud could not read required game data files. This likely means your game installation is corrupted or incomplete.\n\n" + + "Please repair your installation by right-clicking the login button in XIVLauncher and choosing \"Repair game files\".", + "Dalamud"); + + return; + } Log.Information("Lumina is ready: {0}", this.GameData.DataPath); @@ -71,7 +84,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager dalamud.StartInfo.TroubleshootingPackData); this.HasModifiedGameDataFiles = tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception; - + if (this.HasModifiedGameDataFiles) Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData); } @@ -130,7 +143,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager #region Lumina Wrappers /// - public ExcelSheet GetExcelSheet(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow + public ExcelSheet GetExcelSheet(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow => this.Excel.GetSheet(language?.ToLumina(), name); /// @@ -138,7 +151,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager => this.Excel.GetSubrowSheet(language?.ToLumina(), name); /// - public FileResource? GetFile(string path) + public FileResource? GetFile(string path) => this.GetFile(path); /// @@ -161,7 +174,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager : Task.FromException(new FileNotFoundException("The file could not be found.")); /// - public bool FileExists(string path) + public bool FileExists(string path) => this.GameData.FileExists(path); #endregion From b8724f7a59b5cb3dd0b454dff384b7c0c7b0d355 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:44:35 -0800 Subject: [PATCH 219/477] Fix copy paste error --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index e38f56921..d3d0fcebe 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -37,7 +37,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. - /// + ///
/// Mapping is: EventType -> AddonName -> ListenerList internal Dictionary>> EventListeners { get; } = []; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 54c91248e..db698e626 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -167,7 +167,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); try { From 78ecb721cd3f48fd06d72b1d850a47ff68f39d1f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 29 Nov 2025 12:47:45 +0000 Subject: [PATCH 220/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 0769d1f18..e5f586630 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 0769d1f180f859688f47a7a99610e9ce10da946c +Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77 From d12a9ec7da4c2e91c22c0d3f848197eb743ba03d Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 29 Nov 2025 19:15:37 +0100 Subject: [PATCH 221/477] Remove DalamudBetaKey, DalamudBetaKind from config Fix all code that depends on it to use Util.GetActiveTrack() instead --- .../Internal/DalamudConfiguration.cs | 10 ---------- Dalamud/Interface/Internal/DalamudInterface.cs | 2 +- .../Internal/Windows/BranchSwitcherWindow.cs | 18 ++++++++---------- .../PluginInstaller/PluginInstallerWindow.cs | 9 ++++----- Dalamud/Support/DalamudReleases.cs | 15 ++++++++++----- Dalamud/Support/Troubleshooting.cs | 2 +- 6 files changed, 24 insertions(+), 32 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index da0d7c2c6..9404b5b10 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -108,11 +108,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService ///
public bool DoPluginTest { get; set; } = false; - /// - /// Gets or sets a key to opt into Dalamud staging builds. - /// - public string? DalamudBetaKey { get; set; } = null; - /// /// Gets or sets a list of custom repos. /// @@ -278,11 +273,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService ///
public bool IsResumeGameAfterPluginLoad { get; set; } = false; - /// - /// Gets or sets the kind of beta to download when matches the server value. - /// - public string? DalamudBetaKind { get; set; } - /// /// Gets or sets a value indicating whether any plugin should be loaded when the game is started. /// It is reset immediately when read. diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 202334580..64e1acaa4 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -182,7 +182,7 @@ internal class DalamudInterface : IInternalDisposableService () => Service.GetNullable()?.ToggleDevMenu(), VirtualKey.SHIFT); - if (!configuration.DalamudBetaKind.IsNullOrEmpty()) + if (Util.GetActiveTrack() != "release") { titleScreenMenu.AddEntryCore( Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index 4e95b718e..9cc14ea14 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -6,7 +6,6 @@ using System.Net.Http.Json; using System.Threading.Tasks; using Dalamud.Bindings.ImGui; -using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; @@ -15,6 +14,8 @@ using Dalamud.Utility; using Newtonsoft.Json; +using Serilog; + namespace Dalamud.Interface.Internal.Windows; /// @@ -47,7 +48,7 @@ public class BranchSwitcherWindow : Window Debug.Assert(this.branches != null, "this.branches != null"); var trackName = Util.GetActiveTrack(); - this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track != trackName); + this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track == trackName); if (this.selectedBranchIndex == -1) { this.selectedBranchIndex = 0; @@ -86,12 +87,9 @@ public class BranchSwitcherWindow : Window if (ImGui.Button("Pick & Restart"u8)) { - var config = Service.Get(); - config.DalamudBetaKind = pickedBranch.Key; - config.DalamudBetaKey = pickedBranch.Value.Key; - - // If we exit immediately, we need to write out the new config now - config.ForceSave(); + var newTrackName = pickedBranch.Key; + var newTrackKey = pickedBranch.Value.Key; + Log.Verbose("Switching to branch {Branch} with key {Key}", newTrackName, newTrackKey); var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var xlPath = Path.Combine(appData, "XIVLauncher", "current", "XIVLauncher.exe"); @@ -104,8 +102,8 @@ public class BranchSwitcherWindow : Window UseShellExecute = false, }; - ps.ArgumentList.Add($"--dalamud-beta-kind={config.DalamudBetaKind}"); - ps.ArgumentList.Add($"--dalamud-beta-key={(config.DalamudBetaKey.IsNullOrEmpty() ? "invalid" : config.DalamudBetaKey)}"); + ps.ArgumentList.Add($"--dalamud-beta-kind={newTrackName}"); + ps.ArgumentList.Add($"--dalamud-beta-key={(newTrackKey.IsNullOrEmpty() ? "invalid" : newTrackKey)}"); Process.Start(ps); Environment.Exit(0); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index b203b3894..ac092bd25 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -302,8 +302,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.profileManagerWidget.Reset(); - var config = Service.Get(); - if (this.staleDalamudNewVersion == null && !config.DalamudBetaKind.IsNullOrEmpty()) + if (this.staleDalamudNewVersion == null && !Util.GetActiveTrack().IsNullOrEmpty()) { Service.Get().GetVersionForCurrentTrack().ContinueWith(t => { @@ -311,10 +310,10 @@ internal class PluginInstallerWindow : Window, IDisposable return; var versionInfo = t.Result; - if (versionInfo.AssemblyVersion != Util.GetScmVersion() && - versionInfo.Track != "release" && - string.Equals(versionInfo.Key, config.DalamudBetaKey, StringComparison.OrdinalIgnoreCase)) + if (versionInfo.AssemblyVersion != Util.GetScmVersion()) + { this.staleDalamudNewVersion = versionInfo.AssemblyVersion; + } }); } } diff --git a/Dalamud/Support/DalamudReleases.cs b/Dalamud/Support/DalamudReleases.cs index 15e851da2..603c77487 100644 --- a/Dalamud/Support/DalamudReleases.cs +++ b/Dalamud/Support/DalamudReleases.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Networking.Http; +using Dalamud.Utility; using Newtonsoft.Json; @@ -15,7 +16,7 @@ namespace Dalamud.Support; internal class DalamudReleases : IServiceType { private const string VersionInfoUrl = "https://kamori.goats.dev/Dalamud/Release/VersionInfo?track={0}"; - + private readonly HappyHttpClient httpClient; private readonly DalamudConfiguration config; @@ -30,20 +31,24 @@ internal class DalamudReleases : IServiceType this.httpClient = httpClient; this.config = config; } - + /// /// Get the latest version info for the current track. /// /// The version info for the current track. - public async Task GetVersionForCurrentTrack() + public async Task GetVersionForCurrentTrack() { - var url = string.Format(VersionInfoUrl, [this.config.DalamudBetaKind]); + var currentTrack = Util.GetActiveTrack(); + if (currentTrack.IsNullOrEmpty()) + return null; + + var url = string.Format(VersionInfoUrl, [currentTrack]); var response = await this.httpClient.SharedHttpClient.GetAsync(url); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(content); } - + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "laziness")] public class DalamudVersionInfo { diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index 4af8d5ffc..88048c462 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -73,7 +73,7 @@ public static class Troubleshooting DalamudGitHash = Util.GetGitHash() ?? "Unknown", GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown", Language = startInfo.Language.ToString(), - BetaKey = configuration.DalamudBetaKey, + BetaKey = Util.GetActiveTrack(), DoPluginTest = configuration.DoPluginTest, LoadAllApiLevels = pluginManager?.LoadAllApiLevels == true, InterfaceLoaded = interfaceManager?.IsReady ?? false, From 7510c032cc0e706787259b44d4ab2415cc74c8a3 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 29 Nov 2025 19:22:28 +0100 Subject: [PATCH 222/477] Disable Intel CET support, causes CLR crashes on unpatched Windows --- Directory.Build.props | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index 4ed87c809..f9f061c17 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,6 +6,10 @@ x64 x64 13.0 + + + + false From fadf941fa47f5d8775f157a64a5414bfcb00faab Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 30 Nov 2025 02:01:01 +0100 Subject: [PATCH 223/477] Re-add config properties for XLCore/XoM backwards compatibility --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 9404b5b10..d546dc517 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -487,6 +487,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f); +#pragma warning disable SA1600 +#pragma warning disable SA1516 + // XLCore/XoM compatibility until they move it out + public string? DalamudBetaKey { get; set; } = null; + public string? DalamudBetaKind { get; set; } +#pragma warning restore SA1516 +#pragma warning restore SA1600 + /// /// Load a configuration from the provided path. /// From ead1c705a427ee596ca5a4eff741b0d68fe07fc0 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 29 Nov 2025 17:07:51 -0800 Subject: [PATCH 224/477] fix: Route URIs to the specified InternalName --- Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs index 4dbe3fdf1..3b7f18437 100644 --- a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs +++ b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs @@ -45,7 +45,7 @@ public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler private void HandleUri(DalamudUri uri) { - var target = uri.Path.Split("/").FirstOrDefault(); + var target = uri.Path.Split("/").ElementAtOrDefault(1); var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName); if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase)) { From ac2d522415b9a5ccec7a8c5cead997413412a699 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 30 Nov 2025 02:47:07 +0100 Subject: [PATCH 225/477] build: 13.0.0.12 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index ce140b8c9..d1f730d5e 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.11 + 13.0.0.12 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 874745651b1be57b391a078c627a66d7932e30aa Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 29 Nov 2025 21:07:51 -0800 Subject: [PATCH 226/477] feat: Add PID, process time, rename ClientIdentifer to ClientState --- .../Rpc/Service/ClientHelloService.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs index 041bc135f..c5a4c851a 100644 --- a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs +++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Diagnostics; +using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Game; @@ -39,7 +40,9 @@ internal sealed class ClientHelloService : IInternalDisposableService ApiVersion = "1.0", DalamudVersion = Util.GetScmVersion(), GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", - ClientIdentifier = await this.GetClientIdentifier(), + ProcessId = Environment.ProcessId, + ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(), + ClientState = await this.GetClientIdentifier(), }; } @@ -114,7 +117,17 @@ internal record ClientHelloResponse public string? GameVersion { get; init; } /// - /// Gets an identifier for this client. + /// Gets the process ID of this client. /// - public string? ClientIdentifier { get; init; } + public int? ProcessId { get; init; } + + /// + /// Gets the time this process started. + /// + public long? ProcessStartTime { get; init; } + + /// + /// Gets a state for this client for user display. + /// + public string? ClientState { get; init; } } From c51e65e0bd07bc58bfab65128cb7719ce56c996b Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:08:40 -0800 Subject: [PATCH 227/477] Better unload --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 33 +++++-------------- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 15 +++------ 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index d3d0fcebe..5d121bea4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -19,13 +19,13 @@ namespace Dalamud.Game.Addon.Lifecycle; [ServiceManager.EarlyLoadedService] internal unsafe class AddonLifecycle : IInternalDisposableService { + /// + /// Gets a list of all allocated addon virtual tables. + /// + public static readonly List AllocatedTables = []; + private static readonly ModuleLog Log = new("AddonLifecycle"); - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - - private readonly Dictionary modifiedTables = []; - private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] @@ -47,13 +47,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - this.framework.RunOnFrameworkThread(() => - { - foreach (var virtualTable in this.modifiedTables.Values) - { - virtualTable.Dispose(); - } - }); + AllocatedTables.ForEach(entry => entry.Dispose()); + AllocatedTables.Clear(); } /// @@ -141,18 +136,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.LogInitialize(addon->NameString); - if (!this.modifiedTables.ContainsKey(addon->NameString)) - { - // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions - var managedVirtualTableEntry = new AddonVirtualTable(addon, this) - { - // This event is invoked when the game itself has disposed of an addon - // We can use this to know when to remove our virtual table entry - OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString), - }; - - this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry); - } + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + AllocatedTables.Add(new AddonVirtualTable(addon, this)); } catch (Exception e) { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index db698e626..d91cd648f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Logging.Internal; @@ -108,17 +109,11 @@ internal unsafe class AddonVirtualTable : IDisposable this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); } - /// - /// Gets an event that is invoked when this addon's Finalize method is called from native. - /// - public required Action OnAddonFinalized { get; init; } - - /// - /// WARNING! This should not be called at any time except during dalamud unload. - /// + /// public void Dispose() { - this.atkUnitBase->VirtualTable = this.originalVirtualTable; + // Ensure restoration is done atomically. + Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable); IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); } @@ -131,7 +126,7 @@ internal unsafe class AddonVirtualTable : IDisposable if ((freeFlags & 1) == 1) { IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); - this.OnAddonFinalized(); + AddonLifecycle.AllocatedTables.Remove(this); } return result; From 26f119096bad6fd3111a6c5f0ad977f53e396384 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:39:35 -0800 Subject: [PATCH 228/477] Bunch of stuff... --- Dalamud/Configuration/PluginConfigurations.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 31 ++----------------- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 9 ++++-- .../AddonArgTypes/AddonFinalizeArgs.cs | 7 +++-- .../AddonArgTypes/AddonGenericArgs.cs | 3 +- .../AddonArgTypes/AddonReceiveEventArgs.cs | 18 +++-------- .../AddonArgTypes/AddonRefreshArgs.cs | 15 +++------ .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 11 +------ .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 15 +++------ .../AddonArgTypes/AddonUpdateArgs.cs | 26 +++++++--------- .../AddonLifecycleAddressResolver.cs | 2 +- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 14 --------- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +- Dalamud/Interface/Animation/Easing.cs | 2 +- ...ToDoAttribute.cs => Api14ToDoAttribute.cs} | 6 ++-- Dalamud/Utility/Api15ToDoAttribute.cs | 25 +++++++++++++++ Dalamud/Utility/Util.cs | 2 +- 17 files changed, 74 insertions(+), 116 deletions(-) rename Dalamud/Utility/{Api13ToDoAttribute.cs => Api14ToDoAttribute.cs} (75%) create mode 100644 Dalamud/Utility/Api15ToDoAttribute.cs diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index fa2969d31..c01ab2af0 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -11,7 +11,7 @@ namespace Dalamud.Configuration; /// /// Configuration to store settings for a dalamud plugin. /// -[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] +[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 0b2ae1178..62ca47238 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -33,41 +33,14 @@ public abstract class AddonArgs /// public abstract AddonArgsType Type { get; } - /// - /// Checks if addon name matches the given span of char. - /// - /// The name to check. - /// Whether it is the case. - internal bool IsAddon(string name) - { - if (this.Addon.IsNull) - return false; - - if (name.Length is 0 or > 32) - return false; - - if (string.IsNullOrEmpty(this.Addon.Name)) - return false; - - return name == this.Addon.Name; - } - - /// - /// Clears this AddonArgs values. - /// - internal virtual void Clear() - { - this.addonName = null; - this.Addon = 0; - } - /// /// Helper method for ensuring the name of the addon is valid. /// /// The name of the addon for this object. when invalid. private string GetAddonName() { - if (this.Addon.IsNull) return InvalidAddon; + if (this.Addon.IsNull) + return InvalidAddon; var name = this.Addon.Name; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 7254ba7b3..a834d2983 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -1,15 +1,18 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Utility; + +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Draw events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonDrawArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonDrawArgs() + internal AddonDrawArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index 12def3ad3..11d15a081 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -1,15 +1,18 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonFinalizeArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonFinalizeArgs() + internal AddonFinalizeArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs index f3078af69..a20e9d23b 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs @@ -8,8 +8,7 @@ public class AddonGenericArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonGenericArgs() + internal AddonGenericArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 05f51b118..bb8168075 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// @@ -8,8 +10,7 @@ public class AddonReceiveEventArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonReceiveEventArgs() + internal AddonReceiveEventArgs() { } @@ -32,17 +33,8 @@ public class AddonReceiveEventArgs : AddonArgs public nint AtkEvent { get; set; } /// - /// Gets or sets the pointer to a block of data for this event message. + /// Gets or sets the pointer to an AtkEventData for this event message. /// + [Api14ToDo("Rename to AtkEventData")] public nint Data { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - this.AtkEventType = 0; - this.EventParam = 0; - this.AtkEvent = 0; - this.Data = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index c01c065c1..8af017318 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRefreshArgs() + internal AddonRefreshArgs() { } @@ -31,13 +32,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = 0; - this.AtkValues = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index bf00c5d6e..7005b77c2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -8,8 +8,7 @@ public class AddonRequestedUpdateArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRequestedUpdateArgs() + internal AddonRequestedUpdateArgs() { } @@ -25,12 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs /// Gets or sets the StringArrayData** for this event. /// public nint StringArrayData { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - this.NumberArrayData = 0; - this.StringArrayData = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 9b7e86a61..9fd7b6dd0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonSetupArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonSetupArgs() + internal AddonSetupArgs() { } @@ -31,13 +32,7 @@ public class AddonSetupArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = 0; - this.AtkValues = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index bab62fc89..e6147d0eb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -1,39 +1,35 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonUpdateArgs() + internal AddonUpdateArgs() { } /// public override AddonArgsType Type => AddonArgsType.Update; - /// - /// Gets the time since the last update. - /// - public float TimeDelta - { - get => this.TimeDeltaInternal; - init => this.TimeDeltaInternal = value; - } - /// /// Gets or sets the time since the last update. /// internal float TimeDeltaInternal { get; set; } - /// - internal override void Clear() + /// + /// Gets the time since the last update. + /// + private float TimeDelta { - base.Clear(); - this.TimeDeltaInternal = 0; + get => this.TimeDeltaInternal; + init => this.TimeDeltaInternal = value; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 9359870a5..2fa3c5b91 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] +[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index d91cd648f..49ffdc7fb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -26,8 +26,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonLifecycle lifecycleService; - // Obsolete warning is only to prevent users from creating their own event objects. -#pragma warning disable CS0618 // Type or member is obsolete private readonly AddonSetupArgs addonSetupArg = new(); private readonly AddonFinalizeArgs addonFinalizeArg = new(); private readonly AddonDrawArgs addonDrawArg = new(); @@ -36,7 +34,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); private readonly AddonGenericArgs addonGenericArg = new(); -#pragma warning restore CS0618 // Type or member is obsolete private readonly AtkUnitBase* atkUnitBase; @@ -136,7 +133,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; this.addonSetupArg.AtkValueCount = valueCount; this.addonSetupArg.AtkValues = (nint)values; @@ -160,7 +156,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); @@ -178,7 +173,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); @@ -198,7 +192,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; this.addonUpdateArg.TimeDeltaInternal = delta; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); @@ -221,7 +214,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonRefreshArg.Clear(); this.addonRefreshArg.Addon = addon; this.addonRefreshArg.AtkValueCount = valueCount; this.addonRefreshArg.AtkValues = (nint)values; @@ -246,7 +238,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; @@ -270,7 +261,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; this.addonReceiveEventArg.AtkEventType = (byte)eventType; this.addonReceiveEventArg.EventParam = eventParam; @@ -300,7 +290,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); @@ -324,7 +313,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); @@ -346,7 +334,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); @@ -366,7 +353,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index f5b7011fe..af85f9228 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry } /// - [Api13ToDo("Maybe make this config scoped to internal name?")] + [Api14ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index 0d2057b3b..cc1f48ce7 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -48,7 +48,7 @@ public abstract class Easing /// Gets the current value of the animation, following unclamped logic. /// [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] - [Api13ToDo("Map this field to ValueClamped, probably.")] + [Api14ToDo("Map this field to ValueClamped, probably.")] public double Value => this.ValueUnclamped; /// diff --git a/Dalamud/Utility/Api13ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs similarity index 75% rename from Dalamud/Utility/Api13ToDoAttribute.cs rename to Dalamud/Utility/Api14ToDoAttribute.cs index 576401cda..945b6e4db 100644 --- a/Dalamud/Utility/Api13ToDoAttribute.cs +++ b/Dalamud/Utility/Api14ToDoAttribute.cs @@ -4,7 +4,7 @@ namespace Dalamud.Utility; /// Utility class for marking something to be changed for API 13, for ease of lookup. /// [AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api13ToDoAttribute : Attribute +internal sealed class Api14ToDoAttribute : Attribute { /// /// Marks that this should be made internal. @@ -12,11 +12,11 @@ internal sealed class Api13ToDoAttribute : Attribute public const string MakeInternal = "Make internal."; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The explanation. /// The explanation 2. - public Api13ToDoAttribute(string what, string what2 = "") + public Api14ToDoAttribute(string what, string what2 = "") { _ = what; _ = what2; diff --git a/Dalamud/Utility/Api15ToDoAttribute.cs b/Dalamud/Utility/Api15ToDoAttribute.cs new file mode 100644 index 000000000..646c260e8 --- /dev/null +++ b/Dalamud/Utility/Api15ToDoAttribute.cs @@ -0,0 +1,25 @@ +namespace Dalamud.Utility; + +/// +/// Utility class for marking something to be changed for API 13, for ease of lookup. +/// Intended to represent not the upcoming API, but the one after it for more major changes. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class Api15ToDoAttribute : Attribute +{ + /// + /// Marks that this should be made internal. + /// + public const string MakeInternal = "Make internal."; + + /// + /// Initializes a new instance of the class. + /// + /// The explanation. + /// The explanation 2. + public Api15ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 2a3733303..ba31f47e5 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -79,7 +79,7 @@ public static partial class Util /// /// Gets the Dalamud version. /// - [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] + [Api14ToDo("Remove. Make both versions here internal. Add an API somewhere.")] public static string AssemblyVersion { get; } = Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); From 54bac7f32a7efe5268d8fa3697f185184134bab7 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 17:27:48 -0800 Subject: [PATCH 229/477] Refactor Addon Lifecycle --- .../Game/Addon/AddonLifecyclePooledArgs.cs | 107 ----- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +- .../AddonArgTypes/AddonFinalizeArgs.cs | 8 +- .../AddonArgTypes/AddonGenericArgs.cs | 18 + .../AddonArgTypes/AddonReceiveEventArgs.cs | 16 +- .../AddonArgTypes/AddonRefreshArgs.cs | 12 +- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 12 +- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 12 +- .../AddonArgTypes/AddonUpdateArgs.cs | 10 +- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +- Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 54 ++- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 298 ++----------- .../AddonLifecycleAddressResolver.cs | 38 +- .../AddonLifecycleReceiveEventListener.cs | 112 ----- .../Game/Addon/Lifecycle/AddonSetupHook.cs | 80 ---- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 405 ++++++++++++++++++ Dalamud/Hooking/Internal/CallHook.cs | 100 ----- .../Data/Widgets/AddonLifecycleWidget.cs | 51 --- 19 files changed, 543 insertions(+), 825 deletions(-) delete mode 100644 Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs delete mode 100644 Dalamud/Hooking/Internal/CallHook.cs diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs deleted file mode 100644 index 14def2036..000000000 --- a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Threading; - -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -namespace Dalamud.Game.Addon; - -/// Argument pool for Addon Lifecycle services. -[ServiceManager.EarlyLoadedService] -internal sealed class AddonLifecyclePooledArgs : IServiceType -{ - private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64]; - private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64]; - private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64]; - private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64]; - private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64]; - private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64]; - private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64]; - - [ServiceManager.ServiceConstructor] - private AddonLifecyclePooledArgs() - { - } - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonRequestedUpdateArgs arg) => - new(out arg, this.addonRequestedUpdateArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonReceiveEventArgs arg) => - new(out arg, this.addonReceiveEventArgPool); - - /// Returns the object to the pool on dispose. - /// The type. - public readonly ref struct PooledEntry - where T : AddonArgs, new() - { - private readonly Span pool; - private readonly T obj; - - /// Initializes a new instance of the struct. - /// An instance of the argument. - /// The pool to rent from and return to. - public PooledEntry(out T arg, Span pool) - { - this.pool = pool; - foreach (ref var item in pool) - { - if (Interlocked.Exchange(ref item, null) is { } v) - { - this.obj = arg = v; - return; - } - } - - this.obj = arg = new(); - } - - /// Returns the item to the pool. - public void Dispose() - { - var tmp = this.obj; - foreach (ref var item in this.pool) - { - if (Interlocked.Exchange(ref item, tmp) is not { } tmp2) - return; - tmp = tmp2; - } - } - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index c008db08f..0b2ae1178 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Base class for AddonLifecycle AddonArgTypes. /// -public abstract unsafe class AddonArgs +public abstract class AddonArgs { /// /// Constant string representing the name of an addon that is invalid. diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 989e11912..7254ba7b3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -3,7 +3,7 @@ /// /// Addon argument data for Draw events. /// -public class AddonDrawArgs : AddonArgs, ICloneable +public class AddonDrawArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -15,10 +15,4 @@ public class AddonDrawArgs : AddonArgs, ICloneable /// public override AddonArgsType Type => AddonArgsType.Draw; - - /// - public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index d9401b414..12def3ad3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonFinalizeArgs : AddonArgs, ICloneable +public class AddonFinalizeArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -15,10 +15,4 @@ public class AddonFinalizeArgs : AddonArgs, ICloneable /// public override AddonArgsType Type => AddonArgsType.Finalize; - - /// - public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs new file mode 100644 index 000000000..f3078af69 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Draw events. +/// +public class AddonGenericArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonGenericArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Generic; +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 980fe4f2f..05f51b118 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonReceiveEventArgs : AddonArgs, ICloneable +public class AddonReceiveEventArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -36,19 +36,13 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable /// public nint Data { get; set; } - /// - public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkEventType = default; - this.EventParam = default; - this.AtkEvent = default; - this.Data = default; + this.AtkEventType = 0; + this.EventParam = 0; + this.AtkEvent = 0; + this.Data = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index d28631c3c..c01c065c1 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Refresh events. /// -public class AddonRefreshArgs : AddonArgs, ICloneable +public class AddonRefreshArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -33,17 +33,11 @@ public class AddonRefreshArgs : AddonArgs, ICloneable /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; + this.AtkValueCount = 0; + this.AtkValues = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index e87a980fd..bf00c5d6e 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for OnRequestedUpdate events. /// -public class AddonRequestedUpdateArgs : AddonArgs, ICloneable +public class AddonRequestedUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -26,17 +26,11 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable /// public nint StringArrayData { get; set; } - /// - public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.NumberArrayData = default; - this.StringArrayData = default; + this.NumberArrayData = 0; + this.StringArrayData = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 0dd9ecee2..9b7e86a61 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Setup events. /// -public class AddonSetupArgs : AddonArgs, ICloneable +public class AddonSetupArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -33,17 +33,11 @@ public class AddonSetupArgs : AddonArgs, ICloneable /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; + this.AtkValueCount = 0; + this.AtkValues = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index a263f6ae4..bab62fc89 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// -public class AddonUpdateArgs : AddonArgs, ICloneable +public class AddonUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -30,16 +30,10 @@ public class AddonUpdateArgs : AddonArgs, ICloneable /// internal float TimeDeltaInternal { get; set; } - /// - public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.TimeDeltaInternal = default; + this.TimeDeltaInternal = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index b58b5f4c7..95dc5f718 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -9,34 +9,39 @@ public enum AddonArgsType /// Contains argument data for Setup. /// Setup, - + /// /// Contains argument data for Update. /// Update, - + /// /// Contains argument data for Draw. - /// + /// Draw, - + /// /// Contains argument data for Finalize. - /// + /// Finalize, - + /// /// Contains argument data for RequestedUpdate. - /// + /// RequestedUpdate, - + /// /// Contains argument data for Refresh. - /// + /// Refresh, - + /// /// Contains argument data for ReceiveEvent. /// ReceiveEvent, + + /// + /// Generic arg type that contains no meaningful data + /// + Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 5fd0ac964..7738d6c6a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -16,7 +16,7 @@ public enum AddonEvent ///
/// PreSetup, - + /// /// An event that is fired after an addon has finished its initial setup. This event is particularly useful for /// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data @@ -64,7 +64,7 @@ public enum AddonEvent /// /// PreFinalize, - + /// /// An event that is fired before a call to is made in response to a /// change in the subscribed or @@ -81,13 +81,13 @@ public enum AddonEvent /// to the Free Company's overview. /// PreRequestedUpdate, - + /// /// An event that is fired after an addon has finished processing an ArrayData update. /// See for more information. /// PostRequestedUpdate, - + /// /// An event that is fired before an addon calls its method. Refreshes are /// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to @@ -96,13 +96,13 @@ public enum AddonEvent /// /// PreRefresh, - + /// /// An event that is fired after an addon has finished its refresh. /// See for more information. /// PostRefresh, - + /// /// An event that is fired before an addon begins processing a user-driven event via /// , such as mousing over an element or clicking a button. This event @@ -112,10 +112,50 @@ public enum AddonEvent /// /// PreReceiveEvent, - + /// /// An event that is fired after an addon finishes calling its method. /// See for more information. /// PostReceiveEvent, + + /// + /// An event that is fired before an addon processes its open method. + /// + PreOpen, + + /// + /// An event that is fired after an addon has processed its open method. + /// + PostOpen, + + /// + /// An even that is fired before an addon processes its close method. + /// + PreClose, + + /// + /// An event that is fired after an addon has processed its close method. + /// + PostClose, + + /// + /// An event that is fired before an addon processes its show method. + /// + PreShow, + + /// + /// An event that is fired after an addon has processed its show method. + /// + PostShow, + + /// + /// An event that is fired before an addon processes its hide method. + /// + PreHide, + + /// + /// An event that is fired after an addon has processed its hide method. + /// + PostHide, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index b44ab8764..cea30d6be 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,16 +1,14 @@ using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Runtime.CompilerServices; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; -using Dalamud.Hooking.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle; @@ -26,69 +24,33 @@ internal unsafe class AddonLifecycle : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); + private readonly Dictionary modifiedTables = []; - private readonly nint disallowedReceiveEventAddress; - - private readonly AddonLifecycleAddressResolver address; - private readonly AddonSetupHook onAddonSetupHook; - private readonly Hook onAddonFinalizeHook; - private readonly CallHook onAddonDrawHook; - private readonly CallHook onAddonUpdateHook; - private readonly Hook onAddonRefreshHook; - private readonly CallHook onAddonRequestedUpdateHook; + private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) { - this.address = new AddonLifecycleAddressResolver(); - this.address.Setup(sigScanner); + this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); + this.onInitializeAddonHook.Enable(); - this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent; - - var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon; - - this.onAddonSetupHook = new AddonSetupHook(this.address.AddonSetup, this.OnAddonSetup); - this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); - this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); - this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); - this.onAddonRefreshHook = Hook.FromAddress(refreshAddonAddress, this.OnAddonRefresh); - this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); - - this.onAddonSetupHook.Enable(); - this.onAddonFinalizeHook.Enable(); - this.onAddonDrawHook.Enable(); - this.onAddonUpdateHook.Enable(); - this.onAddonRefreshHook.Enable(); - this.onAddonRequestedUpdateHook.Enable(); + Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}"); } - private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); - - /// - /// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks. - /// - internal List ReceiveEventListeners { get; } = new(); - /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal List EventListeners { get; } = new(); + internal List EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() { - this.onAddonSetupHook.Dispose(); - this.onAddonFinalizeHook.Dispose(); - this.onAddonDrawHook.Dispose(); - this.onAddonUpdateHook.Dispose(); - this.onAddonRefreshHook.Dispose(); - this.onAddonRequestedUpdateHook.Dispose(); + this.onInitializeAddonHook?.Dispose(); + this.onInitializeAddonHook = null; - foreach (var receiveEventListener in this.ReceiveEventListeners) + foreach (var virtualTable in this.modifiedTables.Values) { - receiveEventListener.Dispose(); + virtualTable.Dispose(); } } @@ -101,16 +63,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.framework.RunOnTick(() => { this.EventListeners.Add(listener); - - // If we want receive event messages have an already active addon, enable the receive event hook. - // If the addon isn't active yet, we'll grab the hook when it sets up. - if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) - { - receiveEventListener.TryEnable(); - } - } }); } @@ -122,24 +74,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. listener.Removed = true; - + this.framework.RunOnTick(() => { this.EventListeners.Remove(listener); - - // If we are disabling an ReceiveEvent listener, check if we should disable the hook. - if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - // Get the ReceiveEvent Listener for this addon - if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) - { - // If there are no other listeners listening for this event, disable the hook. - if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent)) - { - receiveEventListener.Disable(); - } - } - } }); } @@ -160,7 +98,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener. if (listener.Removed) continue; - + // Match on string.empty for listeners that want events for all addons. if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) continue; @@ -176,201 +114,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } } - private void RegisterReceiveEventHook(AtkUnitBase* addon) + private void OnAddonInitialize(AtkUnitBase* addon) { - // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. - // Disallows hooking the core internal event handler. - var addonName = addon->NameString; - var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent; - if (receiveEventAddress != this.disallowedReceiveEventAddress) + try { - // If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler. - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener) + this.LogInitialize(addon->NameString); + + if (!this.modifiedTables.ContainsKey(addon->NameString)) { - if (!existingListener.AddonNames.Contains(addonName)) + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + var managedVirtualTableEntry = new AddonVirtualTable(addon, this) { - existingListener.AddonNames.Add(addonName); - } - } + // This event is invoked when the game itself has disposed of an addon + // We can use this to know when to remove our virtual table entry + OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString), + }; - // Else, we have an addon that we don't have the ReceiveEvent for yet, make it. - else - { - this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress)); - } - - // If we have an active listener for this addon already, we need to activate this hook. - if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName)) - { - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener) - { - receiveEventListener.TryEnable(); - } + this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry); } } + catch (Exception e) + { + Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize."); + } + + this.onInitializeAddonHook!.Original(addon); } - private void UnregisterReceiveEventHook(string addonName) + [Conditional("DEBUG")] + private void LogInitialize(string addonName) { - // Remove this addons ReceiveEvent Registration - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener) - { - eventListener.AddonNames.Remove(addonName); - - // If there are no more listeners let's remove and dispose. - if (eventListener.AddonNames.Count is 0) - { - this.ReceiveEventListeners.Remove(eventListener); - eventListener.Dispose(); - } - } - } - - private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) - { - try - { - this.RegisterReceiveEventHook(addon); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); - } - - using var returner = this.argsPool.Rent(out AddonSetupArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkValueCount = valueCount; - arg.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreSetup, arg); - valueCount = arg.AtkValueCount; - values = (AtkValue*)arg.AtkValues; - - try - { - addon->OnSetup(valueCount, values); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostSetup, arg); - } - - private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) - { - try - { - var addonName = atkUnitBase[0]->NameString; - this.UnregisterReceiveEventHook(addonName); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); - } - - using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg); - arg.Clear(); - arg.Addon = (nint)atkUnitBase[0]; - this.InvokeListenersSafely(AddonEvent.PreFinalize, arg); - - try - { - this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); - } - } - - private void OnAddonDraw(AtkUnitBase* addon) - { - using var returner = this.argsPool.Rent(out AddonDrawArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - this.InvokeListenersSafely(AddonEvent.PreDraw, arg); - - try - { - addon->Draw(); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostDraw, arg); - } - - private void OnAddonUpdate(AtkUnitBase* addon, float delta) - { - using var returner = this.argsPool.Rent(out AddonUpdateArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.TimeDeltaInternal = delta; - this.InvokeListenersSafely(AddonEvent.PreUpdate, arg); - - try - { - addon->Update(delta); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostUpdate, arg); - } - - private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values) - { - var result = false; - - using var returner = this.argsPool.Rent(out AddonRefreshArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkValueCount = valueCount; - arg.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreRefresh, arg); - valueCount = arg.AtkValueCount; - values = (AtkValue*)arg.AtkValues; - - try - { - result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostRefresh, arg); - return result; - } - - private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) - { - using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.NumberArrayData = (nint)numberArrayData; - arg.StringArrayData = (nint)stringArrayData; - this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg); - numberArrayData = (NumberArrayData**)arg.NumberArrayData; - stringArrayData = (StringArrayData**)arg.StringArrayData; - - try - { - addon->OnRequestedUpdate(numberArrayData, stringArrayData); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg); + Log.Debug($"Initializing {addonName}"); } } @@ -387,7 +161,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); - private readonly List eventListeners = new(); + private readonly List eventListeners = []; /// void IInternalDisposableService.DisposeService() @@ -458,7 +232,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi this.eventListeners.RemoveAll(entry => { if (entry.FunctionDelegate != handler) return false; - + this.addonLifecycleService.UnregisterListener(entry); return true; }); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 854d666fd..1d767aac4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -1,56 +1,24 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Utility; namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver +[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")] +internal class AddonLifecycleAddressResolver : BaseAddressResolver { - /// - /// Gets the address of the addon setup hook invoked by the AtkUnitManager. - /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue. - /// This is called for a majority of all addon OnSetup's. - /// - public nint AddonSetup { get; private set; } - - /// - /// Gets the address of the other addon setup hook invoked by the AtkUnitManager. - /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue. - /// This seems to be called rarely for specific addons. - /// - public nint AddonSetup2 { get; private set; } - /// /// Gets the address of the addon finalize hook invoked by the AtkUnitManager. /// public nint AddonFinalize { get; private set; } - /// - /// Gets the address of the addon draw hook invoked by virtual function call. - /// - public nint AddonDraw { get; private set; } - - /// - /// Gets the address of the addon update hook invoked by virtual function call. - /// - public nint AddonUpdate { get; private set; } - - /// - /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. - /// - public nint AddonOnRequestedUpdate { get; private set; } - /// /// Scan for and setup any configured address pointers. /// /// The signature scanner to facilitate setup. protected override void Setup64Bit(ISigScanner sig) { - this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); - this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01"); - this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2"); - this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30"); } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs deleted file mode 100644 index 0d2bcc7f2..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Collections.Generic; - -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; -using Dalamud.Hooking; -using Dalamud.Logging.Internal; - -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent. -/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly. -/// -internal unsafe class AddonLifecycleReceiveEventListener : IDisposable -{ - private static readonly ModuleLog Log = new("AddonLifecycle"); - - [ServiceManager.ServiceDependency] - private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); - - /// - /// Initializes a new instance of the class. - /// - /// AddonLifecycle service instance. - /// Initial Addon Requesting this listener. - /// Address of Addon's ReceiveEvent function. - internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress) - { - this.AddonLifecycle = service; - this.AddonNames = [addonName]; - this.FunctionAddress = receiveEventAddress; - } - - /// - /// Gets the list of addons that use this receive event hook. - /// - public List AddonNames { get; init; } - - /// - /// Gets the address of the ReceiveEvent function as provided by the vtable on setup. - /// - public nint FunctionAddress { get; init; } - - /// - /// Gets the contained hook for these addons. - /// - public Hook? Hook { get; private set; } - - /// - /// Gets or sets the Reference to AddonLifecycle service instance. - /// - private AddonLifecycle AddonLifecycle { get; set; } - - /// - /// Try to hook and enable this receive event handler. - /// - public void TryEnable() - { - this.Hook ??= Hook.FromAddress(this.FunctionAddress, this.OnReceiveEvent); - this.Hook?.Enable(); - } - - /// - /// Disable the hook for this receive event handler. - /// - public void Disable() - { - this.Hook?.Disable(); - } - - /// - public void Dispose() - { - this.Hook?.Dispose(); - } - - private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) - { - // Check that we didn't get here through a call to another addons handler. - var addonName = addon->NameString; - if (!this.AddonNames.Contains(addonName)) - { - this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData); - return; - } - - using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkEventType = (byte)eventType; - arg.EventParam = eventParam; - arg.AtkEvent = (IntPtr)atkEvent; - arg.Data = (nint)atkEventData; - this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg); - eventType = (AtkEventType)arg.AtkEventType; - eventParam = arg.EventParam; - atkEvent = (AtkEvent*)arg.AtkEvent; - atkEventData = (AtkEventData*)arg.Data; - - try - { - this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); - } - - this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg); - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs deleted file mode 100644 index 297323b8f..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Runtime.InteropServices; - -using Reloaded.Hooks.Definitions; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// This class represents a callsite hook used to replace the address of the OnSetup function in r9. -/// -/// Delegate signature for this hook. -internal class AddonSetupHook : IDisposable where T : Delegate -{ - private readonly Reloaded.Hooks.AsmHook asmHook; - - private T? detour; - private bool activated; - - /// - /// Initializes a new instance of the class. - /// - /// Address of the instruction to replace. - /// Delegate to invoke. - internal AddonSetupHook(nint address, T detour) - { - this.detour = detour; - - var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); - var code = new[] - { - "use64", - $"mov r9, 0x{detourPtr:X8}", - }; - - var opt = new AsmHookOptions - { - PreferRelativeJump = true, - Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, - MaxOpcodeSize = 5, - }; - - this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); - } - - /// - /// Gets a value indicating whether the hook is enabled. - /// - public bool IsEnabled => this.asmHook.IsEnabled; - - /// - /// Starts intercepting a call to the function. - /// - public void Enable() - { - if (!this.activated) - { - this.activated = true; - this.asmHook.Activate(); - return; - } - - this.asmHook.Enable(); - } - - /// - /// Stops intercepting a call to the function. - /// - public void Disable() - { - this.asmHook.Disable(); - } - - /// - /// Remove a hook from the current process. - /// - public void Dispose() - { - this.asmHook.Disable(); - this.detour = null; - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs new file mode 100644 index 000000000..58e32a252 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -0,0 +1,405 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Logging.Internal; + +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon.Lifecycle; + +/// +/// Represents a class that holds references to an addons original and modified virtual table entries. +/// +internal unsafe class AddonVirtualTable : IDisposable +{ + // This need to be at minimum the largest virtual table size of all addons + // Copying extra entries is not problematic, and is considered safe. + private const int VirtualTableEntryCount = 200; + + private const bool EnableAdvancedLogging = true; + private const bool EnableSpammyLogging = false; + + private static readonly ModuleLog Log = new("LifecycleVT"); + + private readonly AddonLifecycle lifecycleService; + + // Obsolete warning is only to prevent users from creating their own event objects. +#pragma warning disable CS0618 // Type or member is obsolete + private readonly AddonSetupArgs addonSetupArg = new(); + private readonly AddonFinalizeArgs addonFinalizeArg = new(); + private readonly AddonDrawArgs addonDrawArg = new(); + private readonly AddonUpdateArgs addonUpdateArg = new(); + private readonly AddonRefreshArgs addonRefreshArg = new(); + private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); + private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); + private readonly AddonGenericArgs addonGenericArg = new(); +#pragma warning restore CS0618 // Type or member is obsolete + + private readonly AtkUnitBase* atkUnitBase; + + private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable; + private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable; + + // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table, + // the CLR needs to know they are in use, or it will invalidate them causing random crashing. + private readonly AtkUnitBase.Delegates.Dtor destructorFunction; + private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction; + private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction; + private readonly AtkUnitBase.Delegates.Draw drawFunction; + private readonly AtkUnitBase.Delegates.Update updateFunction; + private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction; + private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction; + private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction; + private readonly AtkUnitBase.Delegates.Open openFunction; + private readonly AtkUnitBase.Delegates.Close closeFunction; + private readonly AtkUnitBase.Delegates.Show showFunction; + private readonly AtkUnitBase.Delegates.Hide hideFunction; + + /// + /// Initializes a new instance of the class. + /// + /// AtkUnitBase* for the addon to replace the table of. + /// Reference to AddonLifecycle service to callback and invoke listeners. + internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService) + { + this.atkUnitBase = addon; + this.lifecycleService = lifecycleService; + + // Save original virtual table + this.originalVirtualTable = addon->VirtualTable; + + // Create copy of original table + // Note this will copy any derived/overriden functions that this specific addon has. + // Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game + this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8); + NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + + // Overwrite the addons existing virtual table with our own + addon->VirtualTable = this.modifiedVirtualTable; + + // Pin each of our listener functions + this.destructorFunction = this.OnAddonDestructor; + this.onSetupFunction = this.OnAddonSetup; + this.finalizerFunction = this.OnAddonFinalize; + this.drawFunction = this.OnAddonDraw; + this.updateFunction = this.OnAddonUpdate; + this.onRefreshFunction = this.OnAddonRefresh; + this.onRequestedUpdateFunction = this.OnRequestedUpdate; + this.onReceiveEventFunction = this.OnAddonReceiveEvent; + this.openFunction = this.OnAddonOpen; + this.closeFunction = this.OnAddonClose; + this.showFunction = this.OnAddonShow; + this.hideFunction = this.OnAddonHide; + + // Overwrite specific virtual table entries + this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); + this.modifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction); + this.modifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction); + this.modifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction); + this.modifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); + this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction); + this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction); + this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction); + this.modifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction); + this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); + this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); + this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); + } + + /// + /// Gets an event that is invoked when this addon's Finalize method is called from native. + /// + public required Action OnAddonFinalized { get; init; } + + /// + /// WARNING! This should not be called at any time except during dalamud unload. + /// + public void Dispose() + { + this.atkUnitBase->VirtualTable = this.originalVirtualTable; + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + } + + private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) + { + this.LogEvent(); + + var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); + + if ((freeFlags & 1) == 1) + { + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + this.OnAddonFinalized(); + } + + return result; + } + + private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + this.LogEvent(); + + this.addonSetupArg.Clear(); + this.addonSetupArg.Addon = addon; + this.addonSetupArg.AtkValueCount = valueCount; + this.addonSetupArg.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg); + valueCount = this.addonSetupArg.AtkValueCount; + values = (AtkValue*)this.addonSetupArg.AtkValues; + + try + { + this.originalVirtualTable->OnSetup(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg); + } + + private void OnAddonFinalize(AtkUnitBase* thisPtr) + { + this.LogEvent(); + + this.addonFinalizeArg.Clear(); + this.addonFinalizeArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg); + + try + { + this.originalVirtualTable->Finalizer(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); + } + } + + private void OnAddonDraw(AtkUnitBase* addon) + { + this.LogEvent(); + + this.addonDrawArg.Clear(); + this.addonDrawArg.Addon = addon; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); + + try + { + this.originalVirtualTable->Draw(addon); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg); + } + + private void OnAddonUpdate(AtkUnitBase* addon, float delta) + { + this.LogEvent(); + + this.addonUpdateArg.Clear(); + this.addonUpdateArg.Addon = addon; + this.addonUpdateArg.TimeDeltaInternal = delta; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); + + try + { + this.originalVirtualTable->Update(addon, delta); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg); + } + + private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + this.LogEvent(); + + var result = false; + + this.addonRefreshArg.Clear(); + this.addonRefreshArg.Addon = addon; + this.addonRefreshArg.AtkValueCount = valueCount; + this.addonRefreshArg.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg); + valueCount = this.addonRefreshArg.AtkValueCount; + values = (AtkValue*)this.addonRefreshArg.AtkValues; + + try + { + result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg); + return result; + } + + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + this.LogEvent(); + + this.addonRequestedUpdateArg.Clear(); + this.addonRequestedUpdateArg.Addon = addon; + this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; + this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg); + numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData; + stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData; + + try + { + this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg); + } + + private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) + { + this.LogEvent(); + + this.addonReceiveEventArg.Clear(); + this.addonReceiveEventArg.Addon = (nint)addon; + this.addonReceiveEventArg.AtkEventType = (byte)eventType; + this.addonReceiveEventArg.EventParam = eventParam; + this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent; + this.addonReceiveEventArg.Data = (nint)atkEventData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg); + eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType; + eventParam = this.addonReceiveEventArg.EventParam; + atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent; + atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data; + + try + { + this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg); + } + + private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) + { + this.LogEvent(); + + var result = false; + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); + + try + { + result = this.originalVirtualTable->Open(thisPtr, depthLayer); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg); + + return result; + } + + private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) + { + this.LogEvent(); + + var result = false; + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); + + try + { + result = this.originalVirtualTable->Close(thisPtr, fireCallback); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg); + + return result; + } + + private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) + { + this.LogEvent(); + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); + + try + { + this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg); + } + + private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) + { + this.LogEvent(); + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); + + try + { + this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg); + } + + [Conditional("DEBUG")] + private void LogEvent([CallerMemberName] string caller = "") + { + if (EnableAdvancedLogging) + { + if (!EnableSpammyLogging) + { + if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") + return; + } + + Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}"); + } + } +} diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs deleted file mode 100644 index 92bc6e31a..000000000 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Runtime.InteropServices; - -using Reloaded.Hooks.Definitions; - -namespace Dalamud.Hooking.Internal; - -/// -/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook. -/// This is a destructive operation, no other callsite hooks can coexist at the same address. -/// -/// There's no .Original for this hook type. -/// This is only intended for be for functions where the parameters provided allow you to invoke the original call. -/// -/// This class was specifically added for hooking virtual function callsites. -/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered. -/// -/// Delegate signature for this hook. -internal class CallHook : IDalamudHook where T : Delegate -{ - private readonly Reloaded.Hooks.AsmHook asmHook; - - private T? detour; - private bool activated; - - /// - /// Initializes a new instance of the class. - /// - /// Address of the instruction to replace. - /// Delegate to invoke. - internal CallHook(nint address, T detour) - { - ArgumentNullException.ThrowIfNull(detour); - - this.detour = detour; - this.Address = address; - - var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); - var code = new[] - { - "use64", - $"mov rax, 0x{detourPtr:X8}", - "call rax", - }; - - var opt = new AsmHookOptions - { - PreferRelativeJump = true, - Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, - MaxOpcodeSize = 5, - }; - - this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); - } - - /// - /// Gets a value indicating whether the hook is enabled. - /// - public bool IsEnabled => this.asmHook.IsEnabled; - - /// - public IntPtr Address { get; } - - /// - public string BackendName => "Reloaded AsmHook"; - - /// - public bool IsDisposed => this.detour == null; - - /// - /// Starts intercepting a call to the function. - /// - public void Enable() - { - if (!this.activated) - { - this.activated = true; - this.asmHook.Activate(); - return; - } - - this.asmHook.Enable(); - } - - /// - /// Stops intercepting a call to the function. - /// - public void Disable() - { - this.asmHook.Disable(); - } - - /// - /// Remove a hook from the current process. - /// - public void Dispose() - { - this.asmHook.Disable(); - this.detour = null; - } -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index b58166e89..c336f895e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -54,13 +52,6 @@ public class AddonLifecycleWidget : IDataWindowWidget this.DrawEventListeners(); ImGui.Unindent(); } - - if (ImGui.CollapsingHeader("ReceiveEvent Hooks"u8)) - { - ImGui.Indent(); - this.DrawReceiveEventHooks(); - ImGui.Unindent(); - } } private void DrawEventListeners() @@ -100,46 +91,4 @@ public class AddonLifecycleWidget : IDataWindowWidget } } } - - private void DrawReceiveEventHooks() - { - if (!this.Ready) return; - - var listeners = this.AddonLifecycle.ReceiveEventListeners; - - if (listeners.Count == 0) - { - ImGui.Text("No ReceiveEvent Hooks are Registered"u8); - } - - foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners) - { - if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames))) - { - ImGui.Columns(2); - - var functionAddress = receiveEventListener.FunctionAddress; - - ImGui.Text("Hook Address"u8); - ImGui.NextColumn(); - ImGui.Text($"0x{functionAddress:X} (ffxiv_dx11.exe+{functionAddress - Process.GetCurrentProcess().MainModule!.BaseAddress:X})"); - - ImGui.NextColumn(); - ImGui.Text("Hook Status"u8); - ImGui.NextColumn(); - if (receiveEventListener.Hook is null) - { - ImGui.Text("Hook is null"u8); - } - else - { - var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed; - var text = receiveEventListener.Hook.IsEnabled ? "Enabled"u8 : "Disabled"u8; - ImGui.TextColored(color, text); - } - - ImGui.Columns(1); - } - } - } } From 27a7adfdb9851173835fb09ec2af0141d4a329e5 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 18:56:34 -0800 Subject: [PATCH 230/477] Minor cleanup --- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 11 ++++++----- .../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 95dc5f718..de32bd254 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -41,7 +41,7 @@ public enum AddonArgsType ReceiveEvent, /// - /// Generic arg type that contains no meaningful data + /// Generic arg type that contains no meaningful data. /// Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cea30d6be..0c23f5661 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -33,8 +33,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); this.onInitializeAddonHook.Enable(); - - Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}"); } /// @@ -48,10 +46,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - foreach (var virtualTable in this.modifiedTables.Values) + this.framework.RunOnFrameworkThread(() => { - virtualTable.Dispose(); - } + foreach (var virtualTable in this.modifiedTables.Values) + { + virtualTable.Dispose(); + } + }); } /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 1d767aac4..9359870a5 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")] +[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 58e32a252..ca5d970ef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,7 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = true; + private const bool EnableAdvancedLogging = false; private const bool EnableSpammyLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); From 0533872a73f2caccc9ce6f6563070554bf7de59f Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 20:45:54 -0800 Subject: [PATCH 231/477] Fix unreachable code complaint --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index ca5d970ef..54c91248e 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,8 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = false; - private const bool EnableSpammyLogging = false; + private const bool EnableLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); @@ -125,7 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); @@ -140,7 +139,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; @@ -164,7 +163,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonFinalize(AtkUnitBase* thisPtr) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; @@ -182,7 +181,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonDraw(AtkUnitBase* addon) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; @@ -202,7 +201,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; @@ -223,7 +222,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -250,7 +249,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; @@ -274,7 +273,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; @@ -302,7 +301,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -326,7 +325,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -350,7 +349,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -370,7 +369,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -389,15 +388,13 @@ internal unsafe class AddonVirtualTable : IDisposable } [Conditional("DEBUG")] - private void LogEvent([CallerMemberName] string caller = "") + private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") { - if (EnableAdvancedLogging) + if (loggingEnabled) { - if (!EnableSpammyLogging) - { - if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") - return; - } + // Manually disable the really spammy log events, you can comment this out if you need to debug them. + if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") + return; Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}"); } From 4f59e0951303b2d4eca5700478982b93d2ca7ab6 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:24:35 -0800 Subject: [PATCH 232/477] Improve LifecycleInvoke efficiency with Dictionary --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 0c23f5661..cf1270803 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal List EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,10 +61,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => - { - this.EventListeners.Add(listener); - }); + this.EventListeners.TryAdd(listener.EventType, [ listener ]); + this.EventListeners[listener.EventType].Add(listener); } /// @@ -73,13 +71,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. - listener.Removed = true; - - this.framework.RunOnTick(() => + if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) { - this.EventListeners.Remove(listener); - }); + listenerList.Remove(listener); + } } /// @@ -90,16 +85,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// What to blame on errors. internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { + // Early return if we don't have any listeners of this type + if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in this.EventListeners) + foreach (var listener in listenerList) { - if (listener.EventType != eventType) - continue; - - // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener. - if (listener.Removed) - continue; - // Match on string.empty for listeners that want events for all addons. if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) continue; From b82b4f40cec89a9c5212c9ec1ba8ac8143352e18 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:30:40 -0800 Subject: [PATCH 233/477] Use hashset to prevent duplicate entries --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cf1270803..403671920 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() From c3e3e4aa8582e83c8efb74b991c59714111411ad Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 08:35:54 -0800 Subject: [PATCH 234/477] Fix accidentally breaking widget --- .../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index c336f895e..73c4e540a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; @@ -58,12 +57,11 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var eventType in Enum.GetValues()) + foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(eventType.ToString())) + if (ImGui.CollapsingHeader(listenerType.ToString())) { ImGui.Indent(); - var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList(); if (listeners.Count == 0) { From f8725e5f37e4a9f6a620eaf328da147124c7e8a9 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:08:24 -0800 Subject: [PATCH 235/477] further improve performance --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 61 ++++++++++++++----- .../Data/Widgets/AddonLifecycleWidget.cs | 40 ++++++------ 2 files changed, 67 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 403671920..e38f56921 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + /// Mapping is: EventType -> AddonName -> ListenerList + internal Dictionary>> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,8 +62,18 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.EventListeners.TryAdd(listener.EventType, [ listener ]); - this.EventListeners[listener.EventType].Add(listener); + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + this.EventListeners.TryAdd(listener.EventType, []); + } + + // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) + { + this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []); + } + + this.EventListeners[listener.EventType][listener.AddonName].Add(listener); } /// @@ -71,9 +82,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) + if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) { - listenerList.Remove(listener); + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + { + addonListener.Remove(listener); + } } } @@ -86,22 +100,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { // Early return if we don't have any listeners of this type - if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; - // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in listenerList) + // Handle listeners for this event type that don't care which addon is triggering it + if (addonListeners.TryGetValue(string.Empty, out var globalListeners)) { - // Match on string.empty for listeners that want events for all addons. - if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) - continue; - - try + foreach (var listener in globalListeners) { - listener.FunctionDelegate.Invoke(eventType, args); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener."); + } } - catch (Exception e) + } + + // Handle listeners that are listening for this addon and event type specifically + if (addonListeners.TryGetValue(args.AddonName, out var addonListener)) + { + foreach (var listener in addonListener) { - Log.Error(e, $"Exception in {blame} during {eventType} invoke."); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}."); + } } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 73c4e540a..0f193556b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -2,7 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -57,35 +58,38 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) + foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(listenerType.ToString())) + using var eventId = ImRaii.PushId(eventType.ToString()); + + if (ImGui.CollapsingHeader(eventType.ToString())) { - ImGui.Indent(); + using var eventIndent = ImRaii.PushIndent(); - if (listeners.Count == 0) + if (addonListeners.Count == 0) { - ImGui.Text("No Listeners Registered for Event"u8); + ImGui.Text("No Addons Registered for Event"u8); } - if (ImGui.BeginTable("AddonLifecycleListenersTable"u8, 2)) + foreach (var (addonName, listeners) in addonListeners) { - ImGui.TableSetupColumn("##AddonName"u8, ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##MethodInvoke"u8, ImGuiTableColumnFlags.WidthStretch); + using var addonId = ImRaii.PushId(addonName); - foreach (var listener in listeners) + if (ImGui.CollapsingHeader(addonName.IsNullOrEmpty() ? "GLOBAL" : addonName)) { - ImGui.TableNextColumn(); - ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); + using var addonIndent = ImRaii.PushIndent(); - ImGui.TableNextColumn(); - ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + if (listeners.Count == 0) + { + ImGui.Text("No Listeners Registered for Event"u8); + } + + foreach (var listener in listeners) + { + ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + } } - - ImGui.EndTable(); } - - ImGui.Unindent(); } } } From e01acb4a80727def0dd77b2e72202404b139f099 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:11:13 -0800 Subject: [PATCH 236/477] Remove redundant header --- .../Windows/Data/Widgets/AddonLifecycleWidget.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 0f193556b..4fb13b81a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -46,18 +46,6 @@ public class AddonLifecycleWidget : IDataWindowWidget return; } - if (ImGui.CollapsingHeader("Listeners"u8)) - { - ImGui.Indent(); - this.DrawEventListeners(); - ImGui.Unindent(); - } - } - - private void DrawEventListeners() - { - if (!this.Ready) return; - foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { using var eventId = ImRaii.PushId(eventType.ToString()); From be3f71dc734c13ed4ad222ce46fcff7338bb0f82 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:44:35 -0800 Subject: [PATCH 237/477] Fix copy paste error --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index e38f56921..d3d0fcebe 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -37,7 +37,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. - /// + ///
/// Mapping is: EventType -> AddonName -> ListenerList internal Dictionary>> EventListeners { get; } = []; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 54c91248e..db698e626 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -167,7 +167,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); try { From eb9555ee22c3d240a7b770f985207d172c6c8284 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:08:40 -0800 Subject: [PATCH 238/477] Better unload --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 33 +++++-------------- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 15 +++------ 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index d3d0fcebe..5d121bea4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -19,13 +19,13 @@ namespace Dalamud.Game.Addon.Lifecycle; [ServiceManager.EarlyLoadedService] internal unsafe class AddonLifecycle : IInternalDisposableService { + /// + /// Gets a list of all allocated addon virtual tables. + /// + public static readonly List AllocatedTables = []; + private static readonly ModuleLog Log = new("AddonLifecycle"); - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - - private readonly Dictionary modifiedTables = []; - private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] @@ -47,13 +47,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - this.framework.RunOnFrameworkThread(() => - { - foreach (var virtualTable in this.modifiedTables.Values) - { - virtualTable.Dispose(); - } - }); + AllocatedTables.ForEach(entry => entry.Dispose()); + AllocatedTables.Clear(); } /// @@ -141,18 +136,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.LogInitialize(addon->NameString); - if (!this.modifiedTables.ContainsKey(addon->NameString)) - { - // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions - var managedVirtualTableEntry = new AddonVirtualTable(addon, this) - { - // This event is invoked when the game itself has disposed of an addon - // We can use this to know when to remove our virtual table entry - OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString), - }; - - this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry); - } + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + AllocatedTables.Add(new AddonVirtualTable(addon, this)); } catch (Exception e) { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index db698e626..d91cd648f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Logging.Internal; @@ -108,17 +109,11 @@ internal unsafe class AddonVirtualTable : IDisposable this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); } - /// - /// Gets an event that is invoked when this addon's Finalize method is called from native. - /// - public required Action OnAddonFinalized { get; init; } - - /// - /// WARNING! This should not be called at any time except during dalamud unload. - /// + /// public void Dispose() { - this.atkUnitBase->VirtualTable = this.originalVirtualTable; + // Ensure restoration is done atomically. + Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable); IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); } @@ -131,7 +126,7 @@ internal unsafe class AddonVirtualTable : IDisposable if ((freeFlags & 1) == 1) { IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); - this.OnAddonFinalized(); + AddonLifecycle.AllocatedTables.Remove(this); } return result; From 08c176828639fefeae28e74030c34112d963cba8 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:39:35 -0800 Subject: [PATCH 239/477] Bunch of stuff... --- Dalamud/Configuration/PluginConfigurations.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 31 ++----------------- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 9 ++++-- .../AddonArgTypes/AddonFinalizeArgs.cs | 7 +++-- .../AddonArgTypes/AddonGenericArgs.cs | 3 +- .../AddonArgTypes/AddonReceiveEventArgs.cs | 18 +++-------- .../AddonArgTypes/AddonRefreshArgs.cs | 15 +++------ .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 11 +------ .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 15 +++------ .../AddonArgTypes/AddonUpdateArgs.cs | 26 +++++++--------- .../AddonLifecycleAddressResolver.cs | 2 +- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 14 --------- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +- Dalamud/Interface/Animation/Easing.cs | 2 +- ...ToDoAttribute.cs => Api14ToDoAttribute.cs} | 6 ++-- Dalamud/Utility/Api15ToDoAttribute.cs | 25 +++++++++++++++ Dalamud/Utility/Util.cs | 2 +- 17 files changed, 74 insertions(+), 116 deletions(-) rename Dalamud/Utility/{Api13ToDoAttribute.cs => Api14ToDoAttribute.cs} (75%) create mode 100644 Dalamud/Utility/Api15ToDoAttribute.cs diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index fa2969d31..c01ab2af0 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -11,7 +11,7 @@ namespace Dalamud.Configuration; /// /// Configuration to store settings for a dalamud plugin. /// -[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] +[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 0b2ae1178..62ca47238 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -33,41 +33,14 @@ public abstract class AddonArgs /// public abstract AddonArgsType Type { get; } - /// - /// Checks if addon name matches the given span of char. - /// - /// The name to check. - /// Whether it is the case. - internal bool IsAddon(string name) - { - if (this.Addon.IsNull) - return false; - - if (name.Length is 0 or > 32) - return false; - - if (string.IsNullOrEmpty(this.Addon.Name)) - return false; - - return name == this.Addon.Name; - } - - /// - /// Clears this AddonArgs values. - /// - internal virtual void Clear() - { - this.addonName = null; - this.Addon = 0; - } - /// /// Helper method for ensuring the name of the addon is valid. /// /// The name of the addon for this object. when invalid. private string GetAddonName() { - if (this.Addon.IsNull) return InvalidAddon; + if (this.Addon.IsNull) + return InvalidAddon; var name = this.Addon.Name; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 7254ba7b3..a834d2983 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -1,15 +1,18 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Utility; + +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Draw events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonDrawArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonDrawArgs() + internal AddonDrawArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index 12def3ad3..11d15a081 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -1,15 +1,18 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonFinalizeArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonFinalizeArgs() + internal AddonFinalizeArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs index f3078af69..a20e9d23b 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs @@ -8,8 +8,7 @@ public class AddonGenericArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonGenericArgs() + internal AddonGenericArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 05f51b118..bb8168075 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// @@ -8,8 +10,7 @@ public class AddonReceiveEventArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonReceiveEventArgs() + internal AddonReceiveEventArgs() { } @@ -32,17 +33,8 @@ public class AddonReceiveEventArgs : AddonArgs public nint AtkEvent { get; set; } /// - /// Gets or sets the pointer to a block of data for this event message. + /// Gets or sets the pointer to an AtkEventData for this event message. /// + [Api14ToDo("Rename to AtkEventData")] public nint Data { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - this.AtkEventType = 0; - this.EventParam = 0; - this.AtkEvent = 0; - this.Data = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index c01c065c1..8af017318 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRefreshArgs() + internal AddonRefreshArgs() { } @@ -31,13 +32,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = 0; - this.AtkValues = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index bf00c5d6e..7005b77c2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -8,8 +8,7 @@ public class AddonRequestedUpdateArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRequestedUpdateArgs() + internal AddonRequestedUpdateArgs() { } @@ -25,12 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs /// Gets or sets the StringArrayData** for this event. /// public nint StringArrayData { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - this.NumberArrayData = 0; - this.StringArrayData = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 9b7e86a61..9fd7b6dd0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonSetupArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonSetupArgs() + internal AddonSetupArgs() { } @@ -31,13 +32,7 @@ public class AddonSetupArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = 0; - this.AtkValues = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index bab62fc89..e6147d0eb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -1,39 +1,35 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonUpdateArgs() + internal AddonUpdateArgs() { } /// public override AddonArgsType Type => AddonArgsType.Update; - /// - /// Gets the time since the last update. - /// - public float TimeDelta - { - get => this.TimeDeltaInternal; - init => this.TimeDeltaInternal = value; - } - /// /// Gets or sets the time since the last update. /// internal float TimeDeltaInternal { get; set; } - /// - internal override void Clear() + /// + /// Gets the time since the last update. + /// + private float TimeDelta { - base.Clear(); - this.TimeDeltaInternal = 0; + get => this.TimeDeltaInternal; + init => this.TimeDeltaInternal = value; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 9359870a5..2fa3c5b91 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] +[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index d91cd648f..49ffdc7fb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -26,8 +26,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonLifecycle lifecycleService; - // Obsolete warning is only to prevent users from creating their own event objects. -#pragma warning disable CS0618 // Type or member is obsolete private readonly AddonSetupArgs addonSetupArg = new(); private readonly AddonFinalizeArgs addonFinalizeArg = new(); private readonly AddonDrawArgs addonDrawArg = new(); @@ -36,7 +34,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); private readonly AddonGenericArgs addonGenericArg = new(); -#pragma warning restore CS0618 // Type or member is obsolete private readonly AtkUnitBase* atkUnitBase; @@ -136,7 +133,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; this.addonSetupArg.AtkValueCount = valueCount; this.addonSetupArg.AtkValues = (nint)values; @@ -160,7 +156,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); @@ -178,7 +173,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); @@ -198,7 +192,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; this.addonUpdateArg.TimeDeltaInternal = delta; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); @@ -221,7 +214,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonRefreshArg.Clear(); this.addonRefreshArg.Addon = addon; this.addonRefreshArg.AtkValueCount = valueCount; this.addonRefreshArg.AtkValues = (nint)values; @@ -246,7 +238,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; @@ -270,7 +261,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; this.addonReceiveEventArg.AtkEventType = (byte)eventType; this.addonReceiveEventArg.EventParam = eventParam; @@ -300,7 +290,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); @@ -324,7 +313,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); @@ -346,7 +334,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); @@ -366,7 +353,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index f5b7011fe..af85f9228 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry } /// - [Api13ToDo("Maybe make this config scoped to internal name?")] + [Api14ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index 0d2057b3b..cc1f48ce7 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -48,7 +48,7 @@ public abstract class Easing /// Gets the current value of the animation, following unclamped logic. /// [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] - [Api13ToDo("Map this field to ValueClamped, probably.")] + [Api14ToDo("Map this field to ValueClamped, probably.")] public double Value => this.ValueUnclamped; /// diff --git a/Dalamud/Utility/Api13ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs similarity index 75% rename from Dalamud/Utility/Api13ToDoAttribute.cs rename to Dalamud/Utility/Api14ToDoAttribute.cs index 576401cda..945b6e4db 100644 --- a/Dalamud/Utility/Api13ToDoAttribute.cs +++ b/Dalamud/Utility/Api14ToDoAttribute.cs @@ -4,7 +4,7 @@ namespace Dalamud.Utility; /// Utility class for marking something to be changed for API 13, for ease of lookup. /// [AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api13ToDoAttribute : Attribute +internal sealed class Api14ToDoAttribute : Attribute { /// /// Marks that this should be made internal. @@ -12,11 +12,11 @@ internal sealed class Api13ToDoAttribute : Attribute public const string MakeInternal = "Make internal."; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The explanation. /// The explanation 2. - public Api13ToDoAttribute(string what, string what2 = "") + public Api14ToDoAttribute(string what, string what2 = "") { _ = what; _ = what2; diff --git a/Dalamud/Utility/Api15ToDoAttribute.cs b/Dalamud/Utility/Api15ToDoAttribute.cs new file mode 100644 index 000000000..646c260e8 --- /dev/null +++ b/Dalamud/Utility/Api15ToDoAttribute.cs @@ -0,0 +1,25 @@ +namespace Dalamud.Utility; + +/// +/// Utility class for marking something to be changed for API 13, for ease of lookup. +/// Intended to represent not the upcoming API, but the one after it for more major changes. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class Api15ToDoAttribute : Attribute +{ + /// + /// Marks that this should be made internal. + /// + public const string MakeInternal = "Make internal."; + + /// + /// Initializes a new instance of the class. + /// + /// The explanation. + /// The explanation 2. + public Api15ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 19610ef64..f6abc336c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -79,7 +79,7 @@ public static partial class Util /// /// Gets the Dalamud version. /// - [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] + [Api14ToDo("Remove. Make both versions here internal. Add an API somewhere.")] public static string AssemblyVersion { get; } = Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); From 386828005b02d16f24f5710e9aec9453aa3faae5 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 12:37:51 -0800 Subject: [PATCH 240/477] Apply breaking changes --- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 38 +++--- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 21 --- .../AddonArgTypes/AddonFinalizeArgs.cs | 21 --- .../AddonArgTypes/AddonGenericArgs.cs | 17 --- .../AddonArgTypes/AddonReceiveEventArgs.cs | 5 +- .../AddonArgTypes/AddonUpdateArgs.cs | 35 ----- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +--- Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 3 - .../Game/Addon/Lifecycle/AddonLifecycle.cs | 6 +- .../AddonLifecycleAddressResolver.cs | 24 ---- .../Lifecycle/AddonLifecycleEventListener.cs | 9 +- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 129 +++++++++--------- .../Windows/Data/Widgets/HookWidget.cs | 36 ++--- .../Internal/Windows/TitleScreenMenuWindow.cs | 4 +- Directory.Build.props | 2 +- 15 files changed, 114 insertions(+), 261 deletions(-) delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 62ca47238..c4a7e8f53 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -5,19 +5,24 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Base class for AddonLifecycle AddonArgTypes. /// -public abstract class AddonArgs +public class AddonArgs { /// /// Constant string representing the name of an addon that is invalid. /// public const string InvalidAddon = "NullAddon"; - private string? addonName; + /// + /// Initializes a new instance of the class. + /// + internal AddonArgs() + { + } /// /// Gets the name of the addon this args referrers to. /// - public string AddonName => this.GetAddonName(); + public string AddonName { get; private set; } = InvalidAddon; /// /// Gets the pointer to the addons AtkUnitBase. @@ -25,28 +30,17 @@ public abstract class AddonArgs public AtkUnitBasePtr Addon { get; - internal set; + internal set + { + field = value; + + if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name)) + this.AddonName = value.Name; + } } /// /// Gets the type of these args. /// - public abstract AddonArgsType Type { get; } - - /// - /// Helper method for ensuring the name of the addon is valid. - /// - /// The name of the addon for this object. when invalid. - private string GetAddonName() - { - if (this.Addon.IsNull) - return InvalidAddon; - - var name = this.Addon.Name; - - if (string.IsNullOrEmpty(name)) - return InvalidAddon; - - return this.addonName ??= name; - } + public virtual AddonArgsType Type => AddonArgsType.Generic; } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs deleted file mode 100644 index a834d2983..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Dalamud.Utility; - -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Draw events. -/// -[Obsolete("Use AddonGenericArgs instead.")] -[Api15ToDo("Remove this")] -public class AddonDrawArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonDrawArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Draw; -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs deleted file mode 100644 index 11d15a081..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Dalamud.Utility; - -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for ReceiveEvent events. -/// -[Obsolete("Use AddonGenericArgs instead.")] -[Api15ToDo("Remove this")] -public class AddonFinalizeArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonFinalizeArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Finalize; -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs deleted file mode 100644 index a20e9d23b..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Draw events. -/// -public class AddonGenericArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonGenericArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Generic; -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index bb8168075..785cd199f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -1,5 +1,3 @@ -using Dalamud.Utility; - namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// @@ -35,6 +33,5 @@ public class AddonReceiveEventArgs : AddonArgs /// /// Gets or sets the pointer to an AtkEventData for this event message. /// - [Api14ToDo("Rename to AtkEventData")] - public nint Data { get; set; } + public nint AtkEventData { get; set; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs deleted file mode 100644 index e6147d0eb..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Dalamud.Utility; - -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Update events. -/// -[Obsolete("Use AddonGenericArgs instead.")] -[Api15ToDo("Remove this")] -public class AddonUpdateArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonUpdateArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Update; - - /// - /// Gets or sets the time since the last update. - /// - internal float TimeDeltaInternal { get; set; } - - /// - /// Gets the time since the last update. - /// - private float TimeDelta - { - get => this.TimeDeltaInternal; - init => this.TimeDeltaInternal = value; - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index de32bd254..9d7815cef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -5,26 +5,16 @@ /// public enum AddonArgsType { + /// + /// Generic arg type that contains no meaningful data. + /// + Generic, + /// /// Contains argument data for Setup. /// Setup, - /// - /// Contains argument data for Update. - /// - Update, - - /// - /// Contains argument data for Draw. - /// - Draw, - - /// - /// Contains argument data for Finalize. - /// - Finalize, - /// /// Contains argument data for RequestedUpdate. /// @@ -39,9 +29,4 @@ public enum AddonArgsType /// Contains argument data for ReceiveEvent. /// ReceiveEvent, - - /// - /// Generic arg type that contains no meaningful data. - /// - Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 7738d6c6a..5ec57b5e3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -29,7 +29,6 @@ public enum AddonEvent /// An event that is fired before an addon begins its update cycle via . This event /// is fired every frame that an addon is loaded, regardless of visibility. /// - /// PreUpdate, /// @@ -42,7 +41,6 @@ public enum AddonEvent /// An event that is fired before an addon begins drawing to screen via . Unlike /// , this event is only fired if an addon is visible or otherwise drawing to screen. /// - /// PreDraw, /// @@ -62,7 +60,6 @@ public enum AddonEvent ///
/// As this is part of the destruction process for an addon, this event does not have an associated Post event. /// - /// PreFinalize, /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 5d121bea4..ddcebe718 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -59,13 +59,15 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { if (!this.EventListeners.ContainsKey(listener.EventType)) { - this.EventListeners.TryAdd(listener.EventType, []); + if (!this.EventListeners.TryAdd(listener.EventType, [])) + return; } // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) { - this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []); + if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) + return; } this.EventListeners[listener.EventType][listener.AddonName].Add(listener); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs deleted file mode 100644 index 2fa3c5b91..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Dalamud.Utility; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// AddonLifecycleService memory address resolver. -/// -[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] -internal class AddonLifecycleAddressResolver : BaseAddressResolver -{ - /// - /// Gets the address of the addon finalize hook invoked by the AtkUnitManager. - /// - public nint AddonFinalize { get; private set; } - - /// - /// Scan for and setup any configured address pointers. - /// - /// The signature scanner to facilitate setup. - protected override void Setup64Bit(ISigScanner sig) - { - this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs index 9d411cdbc..fc82e0582 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs @@ -25,17 +25,12 @@ internal class AddonLifecycleEventListener /// string.Empty if it wants to be called for any addon. /// public string AddonName { get; init; } - - /// - /// Gets or sets a value indicating whether this event has been unregistered. - /// - public bool Removed { get; set; } - + /// /// Gets the event type this listener is looking for. /// public AddonEvent EventType { get; init; } - + /// /// Gets the delegate this listener invokes. /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 49ffdc7fb..1ce145946 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -26,14 +26,18 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonLifecycle lifecycleService; - private readonly AddonSetupArgs addonSetupArg = new(); - private readonly AddonFinalizeArgs addonFinalizeArg = new(); - private readonly AddonDrawArgs addonDrawArg = new(); - private readonly AddonUpdateArgs addonUpdateArg = new(); - private readonly AddonRefreshArgs addonRefreshArg = new(); - private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); - private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); - private readonly AddonGenericArgs addonGenericArg = new(); + // Each addon gets its own set of args that are used to mutate the original call when used in pre-calls + private readonly AddonSetupArgs setupArgs = new(); + private readonly AddonArgs finalizeArgs = new(); + private readonly AddonArgs drawArgs = new(); + private readonly AddonArgs updateArgs = new(); + private readonly AddonRefreshArgs refreshArgs = new(); + private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new(); + private readonly AddonReceiveEventArgs receiveEventArgs = new(); + private readonly AddonArgs openArgs = new(); + private readonly AddonArgs closeArgs = new(); + private readonly AddonArgs showArgs = new(); + private readonly AddonArgs hideArgs = new(); private readonly AtkUnitBase* atkUnitBase; @@ -133,12 +137,13 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonSetupArg.Addon = addon; - this.addonSetupArg.AtkValueCount = valueCount; - this.addonSetupArg.AtkValues = (nint)values; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg); - valueCount = this.addonSetupArg.AtkValueCount; - values = (AtkValue*)this.addonSetupArg.AtkValues; + this.setupArgs.Addon = addon; + this.setupArgs.AtkValueCount = valueCount; + this.setupArgs.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs); + + valueCount = this.setupArgs.AtkValueCount; + values = (AtkValue*)this.setupArgs.AtkValues; try { @@ -149,15 +154,15 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs); } private void OnAddonFinalize(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); - this.addonFinalizeArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); + this.finalizeArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs); try { @@ -173,8 +178,8 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonDrawArg.Addon = addon; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); + this.drawArgs.Addon = addon; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs); try { @@ -185,16 +190,15 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs); } private void OnAddonUpdate(AtkUnitBase* addon, float delta) { this.LogEvent(EnableLogging); - this.addonUpdateArg.Addon = addon; - this.addonUpdateArg.TimeDeltaInternal = delta; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); + this.updateArgs.Addon = addon; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs); try { @@ -205,7 +209,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs); } private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) @@ -214,12 +218,13 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonRefreshArg.Addon = addon; - this.addonRefreshArg.AtkValueCount = valueCount; - this.addonRefreshArg.AtkValues = (nint)values; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg); - valueCount = this.addonRefreshArg.AtkValueCount; - values = (AtkValue*)this.addonRefreshArg.AtkValues; + this.refreshArgs.Addon = addon; + this.refreshArgs.AtkValueCount = valueCount; + this.refreshArgs.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs); + + valueCount = this.refreshArgs.AtkValueCount; + values = (AtkValue*)this.refreshArgs.AtkValues; try { @@ -230,7 +235,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs); return result; } @@ -238,12 +243,13 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonRequestedUpdateArg.Addon = addon; - this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; - this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg); - numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData; - stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData; + this.requestedUpdateArgs.Addon = addon; + this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData; + this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs); + + numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData; + stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData; try { @@ -254,23 +260,24 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs); } private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { this.LogEvent(EnableLogging); - this.addonReceiveEventArg.Addon = (nint)addon; - this.addonReceiveEventArg.AtkEventType = (byte)eventType; - this.addonReceiveEventArg.EventParam = eventParam; - this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent; - this.addonReceiveEventArg.Data = (nint)atkEventData; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg); - eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType; - eventParam = this.addonReceiveEventArg.EventParam; - atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent; - atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data; + this.receiveEventArgs.Addon = (nint)addon; + this.receiveEventArgs.AtkEventType = (byte)eventType; + this.receiveEventArgs.EventParam = eventParam; + this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent; + this.receiveEventArgs.AtkEventData = (nint)atkEventData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs); + + eventType = (AtkEventType)this.receiveEventArgs.AtkEventType; + eventParam = this.receiveEventArgs.EventParam; + atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent; + atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData; try { @@ -281,7 +288,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs); } private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) @@ -290,8 +297,8 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); + this.openArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs); try { @@ -302,7 +309,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs); return result; } @@ -313,8 +320,8 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); + this.closeArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs); try { @@ -325,7 +332,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs); return result; } @@ -334,8 +341,8 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); + this.showArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs); try { @@ -346,15 +353,15 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs); } private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) { this.LogEvent(EnableLogging); - this.addonGenericArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); + this.hideArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs); try { @@ -365,7 +372,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); } [Conditional("DEBUG")] diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index 3ad8f86c2..c5ae1d8f0 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -5,9 +5,8 @@ using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Game; -using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.ClientState; using Dalamud.Hooking; -using FFXIVClientStructs.FFXIV.Component.GUI; using Serilog; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; @@ -17,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying hook information. /// -internal unsafe class HookWidget : IDataWindowWidget +internal class HookWidget : IDataWindowWidget { private readonly List hookStressTestList = []; @@ -32,9 +31,9 @@ internal unsafe class HookWidget : IDataWindowWidget private bool hookStressTestRunning = false; private MessageBoxWDelegate? messageBoxWOriginal; - private AddonFinalizeDelegate? addonFinalizeOriginal; + private HandleZoneInitPacketDelegate? zoneInitOriginal; - private AddonLifecycleAddressResolver? address; + private ClientStateAddressResolver? address; private delegate int MessageBoxWDelegate( IntPtr hWnd, @@ -42,12 +41,12 @@ internal unsafe class HookWidget : IDataWindowWidget [MarshalAs(UnmanagedType.LPWStr)] string caption, MESSAGEBOX_STYLE type); - private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + private delegate void HandleZoneInitPacketDelegate(nint a1, uint localPlayerEntityId, nint packet, byte type); private enum StressTestHookTarget { MessageBoxW, - AddonFinalize, + ZoneInit, Random, } @@ -65,7 +64,7 @@ internal unsafe class HookWidget : IDataWindowWidget { this.Ready = true; - this.address = new AddonLifecycleAddressResolver(); + this.address = new ClientStateAddressResolver(); this.address.Setup(Service.Get()); } @@ -179,7 +178,7 @@ internal unsafe class HookWidget : IDataWindowWidget return target switch { StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)", - StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)", + StressTestHookTarget.ZoneInit => "ZoneInit (Hook)", _ => target.ToString(), }; } @@ -198,15 +197,10 @@ internal unsafe class HookWidget : IDataWindowWidget return result; } - private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) + private void OnZoneInit(IntPtr a1, uint localPlayerEntityId, IntPtr packet, byte type) { - Log.Information("OnAddonFinalize"); - this.addonFinalizeOriginal!(unitManager, atkUnitBase); - } - - private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta) - { - Log.Information("OnAddonUpdate"); + Log.Information("OnZoneInit"); + this.zoneInitOriginal!.Invoke(a1, localPlayerEntityId, packet, type); } private IDalamudHook HookMessageBoxW() @@ -222,11 +216,11 @@ internal unsafe class HookWidget : IDataWindowWidget return hook; } - private IDalamudHook HookAddonFinalize() + private IDalamudHook HookZoneInit() { - var hook = Hook.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize); + var hook = Hook.FromAddress(this.address!.HandleZoneInitPacket, this.OnZoneInit); - this.addonFinalizeOriginal = hook.Original; + this.zoneInitOriginal = hook.Original; hook.Enable(); return hook; } @@ -241,7 +235,7 @@ internal unsafe class HookWidget : IDataWindowWidget return target switch { StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(), - StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(), + StressTestHookTarget.ZoneInit => this.HookZoneInit(), _ => throw new ArgumentOutOfRangeException(nameof(target), target, null), }; } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index e3eb22a04..62f83f82f 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -471,9 +471,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable private unsafe void OnVersionStringDraw(AddonEvent ev, AddonArgs args) { - if (args is not AddonDrawArgs drawArgs) return; + if (ev is not (AddonEvent.PostDraw or AddonEvent.PreDraw)) return; - var addon = drawArgs.Addon.Struct; + var addon = args.Addon.Struct; var textNode = addon->GetTextNodeById(3); // look and feel init. should be harmless to set. diff --git a/Directory.Build.props b/Directory.Build.props index eabb727e8..3897256bf 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ net10.0-windows x64 x64 - 13.0 + 14.0 From d47a41b2953407335fcb136f4843ea8e096306fc Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 12:48:49 -0800 Subject: [PATCH 241/477] Fix NET14 Spans defaulting to ReadOnlySpan --- imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs | 6 +++--- imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs | 6 +++--- imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs | 6 +++--- imgui/Dalamud.Bindings.ImGui/ImU8String.cs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs index 665fa434f..3cf20bb30 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs @@ -238,7 +238,7 @@ public static unsafe partial class ImGui ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vSpeed, vMin, vMax, @@ -251,7 +251,7 @@ public static unsafe partial class ImGui ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vSpeed, vMin, vMax, @@ -264,7 +264,7 @@ public static unsafe partial class ImGui ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vSpeed, vMin, vMax, diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs index fb86096ff..5881ac462 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs @@ -205,7 +205,7 @@ public static unsafe partial class ImGui InputScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref data)), + MemoryMarshal.Cast(new Span(ref data)), step, stepFast, format.MoveOrDefault("%.3f"u8), @@ -219,7 +219,7 @@ public static unsafe partial class ImGui InputScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref data)), + MemoryMarshal.Cast(new Span(ref data)), step, stepFast, format.MoveOrDefault("%.3f"u8), @@ -233,7 +233,7 @@ public static unsafe partial class ImGui InputScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref data)), + MemoryMarshal.Cast(new Span(ref data)), step, stepFast, format.MoveOrDefault("%.3f"u8), diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs index 20ee78ab6..b0c4b7c79 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs @@ -210,7 +210,7 @@ public static unsafe partial class ImGui ImU8String format = default, ImGuiSliderFlags flags = ImGuiSliderFlags.None) => SliderScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vMin, vMax, format.MoveOrDefault("%.3f"u8), @@ -222,7 +222,7 @@ public static unsafe partial class ImGui SliderScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vMin, vMax, format.MoveOrDefault("%.3f"u8), @@ -236,7 +236,7 @@ public static unsafe partial class ImGui SliderScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vMin, vMax, format.MoveOrDefault("%.3f"u8), diff --git a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs index a62152c39..f2b635764 100644 --- a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs +++ b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs @@ -156,7 +156,7 @@ public ref struct ImU8String return this.rentedBuffer is { } buf ? buf.AsSpan() - : MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer))); + : MemoryMarshal.Cast(new Span(ref Unsafe.AsRef(ref this.fixedBuffer))); } } @@ -165,7 +165,7 @@ public ref struct ImU8String private ref byte FixedBufferByteRef => ref this.FixedBufferSpan[0]; private Span FixedBufferSpan => - MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer))); + MemoryMarshal.Cast(new Span(ref Unsafe.AsRef(ref this.fixedBuffer))); public static implicit operator ImU8String(ReadOnlySpan text) => new(text); public static implicit operator ImU8String(ReadOnlyMemory text) => new(text); From 8e8d0246bc40d4b7d172c48ad2bc076882811102 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 14:00:39 -0800 Subject: [PATCH 242/477] Restore original hookwidget logic --- .../Windows/Data/Widgets/HookWidget.cs | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index c5ae1d8f0..f3e25caf8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -5,8 +5,8 @@ using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Game; -using Dalamud.Game.ClientState; using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Component.GUI; using Serilog; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; @@ -16,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying hook information. /// -internal class HookWidget : IDataWindowWidget +internal unsafe class HookWidget : IDataWindowWidget { private readonly List hookStressTestList = []; @@ -31,9 +31,9 @@ internal class HookWidget : IDataWindowWidget private bool hookStressTestRunning = false; private MessageBoxWDelegate? messageBoxWOriginal; - private HandleZoneInitPacketDelegate? zoneInitOriginal; + private AddonFinalizeDelegate? addonFinalizeOriginal; - private ClientStateAddressResolver? address; + private nint address; private delegate int MessageBoxWDelegate( IntPtr hWnd, @@ -41,12 +41,12 @@ internal class HookWidget : IDataWindowWidget [MarshalAs(UnmanagedType.LPWStr)] string caption, MESSAGEBOX_STYLE type); - private delegate void HandleZoneInitPacketDelegate(nint a1, uint localPlayerEntityId, nint packet, byte type); + private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); private enum StressTestHookTarget { MessageBoxW, - ZoneInit, + AddonFinalize, Random, } @@ -54,7 +54,7 @@ internal class HookWidget : IDataWindowWidget public string DisplayName { get; init; } = "Hook"; /// - public string[]? CommandShortcuts { get; init; } = { "hook" }; + public string[]? CommandShortcuts { get; init; } = ["hook"]; /// public bool Ready { get; set; } @@ -64,8 +64,8 @@ internal class HookWidget : IDataWindowWidget { this.Ready = true; - this.address = new ClientStateAddressResolver(); - this.address.Setup(Service.Get()); + var sigScanner = Service.Get(); + this.address = sigScanner.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); } /// @@ -178,7 +178,7 @@ internal class HookWidget : IDataWindowWidget return target switch { StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)", - StressTestHookTarget.ZoneInit => "ZoneInit (Hook)", + StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)", _ => target.ToString(), }; } @@ -197,10 +197,15 @@ internal class HookWidget : IDataWindowWidget return result; } - private void OnZoneInit(IntPtr a1, uint localPlayerEntityId, IntPtr packet, byte type) + private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { - Log.Information("OnZoneInit"); - this.zoneInitOriginal!.Invoke(a1, localPlayerEntityId, packet, type); + Log.Information("OnAddonFinalize"); + this.addonFinalizeOriginal!(unitManager, atkUnitBase); + } + + private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta) + { + Log.Information("OnAddonUpdate"); } private IDalamudHook HookMessageBoxW() @@ -216,11 +221,11 @@ internal class HookWidget : IDataWindowWidget return hook; } - private IDalamudHook HookZoneInit() + private IDalamudHook HookAddonFinalize() { - var hook = Hook.FromAddress(this.address!.HandleZoneInitPacket, this.OnZoneInit); + var hook = Hook.FromAddress(this.address, this.OnAddonFinalize); - this.zoneInitOriginal = hook.Original; + this.addonFinalizeOriginal = hook.Original; hook.Enable(); return hook; } @@ -235,7 +240,7 @@ internal class HookWidget : IDataWindowWidget return target switch { StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(), - StressTestHookTarget.ZoneInit => this.HookZoneInit(), + StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(), _ => throw new ArgumentOutOfRangeException(nameof(target), target, null), }; } From b81cb9c74c7e8fc5423f1568904d807daeca4115 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 14:07:44 -0800 Subject: [PATCH 243/477] Remove generic args class --- .../Lifecycle/AddonArgTypes/AddonGenericArgs.cs | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs deleted file mode 100644 index a20e9d23b..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Draw events. -/// -public class AddonGenericArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonGenericArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Generic; -} From 2e246967317427d2d5749d69b24a3e8ebc77d328 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 14:47:24 -0800 Subject: [PATCH 244/477] Set flags, and unlock size --- Dalamud/Interface/Windowing/Window.cs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 5169b9746..700481ce5 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -458,26 +458,20 @@ public abstract class Window if (this.internalIsPinned || this.internalIsClickthrough) { - if (!this.hasError) - { - flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; - } - else - { - flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize); - } + flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; } if (this.internalIsClickthrough) { - if (!this.hasError) - { - flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs; - } - else - { - flags &= ~(ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs); - } + flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs; + } + + // If we have an error, reset all flags to default, and unlock window size. + if (this.hasError) + { + flags = ImGuiWindowFlags.None; + ImGui.SetNextWindowCollapsed(false, ImGuiCond.Once); + ImGui.SetNextWindowSizeConstraints(Vector2.Zero, Vector2.PositiveInfinity); } if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags)) From 78781c8988bbd67be3ce6514fd2de1282c052163 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 21:43:26 -0800 Subject: [PATCH 245/477] Add Move, MouseOver, MouseOut, Focus --- .../Lifecycle/AddonArgTypes/AddonCloseArgs.cs | 22 ++++ .../Lifecycle/AddonArgTypes/AddonHideArgs.cs | 32 +++++ .../Lifecycle/AddonArgTypes/AddonShowArgs.cs | 27 ++++ Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 15 +++ Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 60 ++++++++- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 118 +++++++++++++++++- 6 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs new file mode 100644 index 000000000..db3e442f8 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Close events. +/// +public class AddonCloseArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonCloseArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Close; + + /// + /// Gets or sets a value indicating whether the window should fire the callback method on close. + /// + public bool FireCallback { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs new file mode 100644 index 000000000..3e3521bd0 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Hide events. +/// +public class AddonHideArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonHideArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Hide; + + /// + /// Gets or sets a value indicating whether to call the hide callback handler when this hides. + /// + public bool CallHideCallback { get; set; } + + /// + /// Gets or sets the flags that the window will set when it Shows/Hides. + /// + public uint SetShowHideFlags { get; set; } + + /// + /// Gets or sets a value indicating whether something for this event message. + /// + internal bool UnknownBool { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs new file mode 100644 index 000000000..3153d1208 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Show events. +/// +public class AddonShowArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonShowArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Show; + + /// + /// Gets or sets a value indicating whether the window should play open sound effects. + /// + public bool SilenceOpenSoundEffect { get; set; } + + /// + /// Gets or sets the flags that the window will unset when it Shows/Hides. + /// + public uint UnsetShowHideFlags { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 9d7815cef..46ee479ac 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -29,4 +29,19 @@ public enum AddonArgsType /// Contains argument data for ReceiveEvent. ///
ReceiveEvent, + + /// + /// Contains argument data for Show. + /// + Show, + + /// + /// Contains argument data for Hide. + /// + Hide, + + /// + /// Contains argument data for Close. + /// + Close, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 5ec57b5e3..3b9c6e867 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -127,32 +127,80 @@ public enum AddonEvent PostOpen, /// - /// An even that is fired before an addon processes its close method. + /// An even that is fired before an addon processes its Close method. /// PreClose, /// - /// An event that is fired after an addon has processed its close method. + /// An event that is fired after an addon has processed its Close method. /// PostClose, /// - /// An event that is fired before an addon processes its show method. + /// An event that is fired before an addon processes its Show method. /// PreShow, /// - /// An event that is fired after an addon has processed its show method. + /// An event that is fired after an addon has processed its Show method. /// PostShow, /// - /// An event that is fired before an addon processes its hide method. + /// An event that is fired before an addon processes its Hide method. /// PreHide, /// - /// An event that is fired after an addon has processed its hide method. + /// An event that is fired after an addon has processed its Hide method. /// PostHide, + + /// + /// An event that is fired before an addon processes its OnMove method. + /// OnMove is triggered only when a move is completed. + /// + PreMove, + + /// + /// An event that is fired after an addon has processed its OnMove method. + /// OnMove is triggered only when a move is completed. + /// + PostMove, + + /// + /// An event that is fired before an addon processes its MouseOver method. + /// + PreMouseOver, + + /// + /// An event that is fired after an addon has processed its MouseOver method. + /// + PostMouseOver, + + /// + /// An event that is fired before an addon processes its MouseOut method. + /// + PreMouseOut, + + /// + /// An event that is fired after an addon has processed its MouseOut method. + /// + PostMouseOut, + + /// + /// An event that is fired before an addon processes its Focus method. + /// + /// + /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows. + /// + PreFocus, + + /// + /// An event that is fired after an addon has processed its Focus method. + /// + /// + /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows. + /// + PostFocus, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 1ce145946..b92466b5a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -20,7 +20,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableLogging = false; + private const bool EnableLogging = true; private static readonly ModuleLog Log = new("LifecycleVT"); @@ -35,9 +35,13 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new(); private readonly AddonReceiveEventArgs receiveEventArgs = new(); private readonly AddonArgs openArgs = new(); - private readonly AddonArgs closeArgs = new(); - private readonly AddonArgs showArgs = new(); - private readonly AddonArgs hideArgs = new(); + private readonly AddonCloseArgs closeArgs = new(); + private readonly AddonShowArgs showArgs = new(); + private readonly AddonHideArgs hideArgs = new(); + private readonly AddonArgs onMoveArgs = new(); + private readonly AddonArgs onMouseOverArgs = new(); + private readonly AddonArgs onMouseOutArgs = new(); + private readonly AddonArgs focusArgs = new(); private readonly AtkUnitBase* atkUnitBase; @@ -58,6 +62,10 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AtkUnitBase.Delegates.Close closeFunction; private readonly AtkUnitBase.Delegates.Show showFunction; private readonly AtkUnitBase.Delegates.Hide hideFunction; + private readonly AtkUnitBase.Delegates.OnMove onMoveFunction; + private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction; + private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction; + private readonly AtkUnitBase.Delegates.Focus focusFunction; /// /// Initializes a new instance of the class. @@ -94,6 +102,10 @@ internal unsafe class AddonVirtualTable : IDisposable this.closeFunction = this.OnAddonClose; this.showFunction = this.OnAddonShow; this.hideFunction = this.OnAddonHide; + this.onMoveFunction = this.OnMove; + this.onMouseOverFunction = this.OnMouseOver; + this.onMouseOutFunction = this.OnMouseOut; + this.focusFunction = this.OnFocus; // Overwrite specific virtual table entries this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); @@ -108,6 +120,10 @@ internal unsafe class AddonVirtualTable : IDisposable this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); + this.modifiedVirtualTable->OnMove = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction); + this.modifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); + this.modifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); + this.modifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); } /// @@ -200,6 +216,9 @@ internal unsafe class AddonVirtualTable : IDisposable this.updateArgs.Addon = addon; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs); + // Note: Do not pass or allow manipulation of delta. + // It's realistically not something that should be needed. + try { this.originalVirtualTable->Update(addon, delta); @@ -321,8 +340,11 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; this.closeArgs.Addon = thisPtr; + this.closeArgs.FireCallback = fireCallback; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs); + fireCallback = this.closeArgs.FireCallback; + try { result = this.originalVirtualTable->Close(thisPtr, fireCallback); @@ -342,8 +364,13 @@ internal unsafe class AddonVirtualTable : IDisposable this.LogEvent(EnableLogging); this.showArgs.Addon = thisPtr; + this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect; + this.showArgs.UnsetShowHideFlags = unsetShowHideFlags; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs); + silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect; + unsetShowHideFlags = this.showArgs.UnsetShowHideFlags; + try { this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); @@ -361,8 +388,15 @@ internal unsafe class AddonVirtualTable : IDisposable this.LogEvent(EnableLogging); this.hideArgs.Addon = thisPtr; + this.hideArgs.UnknownBool = unkBool; + this.hideArgs.CallHideCallback = callHideCallback; + this.hideArgs.SetShowHideFlags = setShowHideFlags; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs); + unkBool = this.hideArgs.UnknownBool; + callHideCallback = this.hideArgs.CallHideCallback; + setShowHideFlags = this.hideArgs.SetShowHideFlags; + try { this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); @@ -375,6 +409,82 @@ internal unsafe class AddonVirtualTable : IDisposable this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); } + private void OnMove(AtkUnitBase* thisPtr) + { + this.LogEvent(EnableLogging); + + this.onMoveArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs); + + try + { + this.originalVirtualTable->OnMove(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original OnMove. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs); + } + + private void OnMouseOver(AtkUnitBase* thisPtr) + { + this.LogEvent(EnableLogging); + + this.onMouseOverArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs); + + try + { + this.originalVirtualTable->OnMouseOver(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original OnMouseOver. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs); + } + + private void OnMouseOut(AtkUnitBase* thisPtr) + { + this.LogEvent(EnableLogging); + + this.onMouseOutArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs); + + try + { + this.originalVirtualTable->OnMouseOut(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original OnMouseOut. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs); + } + + private void OnFocus(AtkUnitBase* thisPtr) + { + this.LogEvent(EnableLogging); + + this.focusArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs); + + try + { + this.originalVirtualTable->Focus(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original OnFocus. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs); + } + [Conditional("DEBUG")] private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") { From c923884626fa52470700a65d5f0c8236c7905238 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 22:15:32 -0800 Subject: [PATCH 246/477] Disable Logging --- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index b92466b5a..6a27cc8f9 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -20,7 +20,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableLogging = true; + private const bool EnableLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); From 85a7c60daedf7bf85dc0b4c914f1ff3fd9689c1f Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 22:20:02 -0800 Subject: [PATCH 247/477] Fix name inconsistency --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 6a27cc8f9..8fbf77534 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -102,10 +102,10 @@ internal unsafe class AddonVirtualTable : IDisposable this.closeFunction = this.OnAddonClose; this.showFunction = this.OnAddonShow; this.hideFunction = this.OnAddonHide; - this.onMoveFunction = this.OnMove; - this.onMouseOverFunction = this.OnMouseOver; - this.onMouseOutFunction = this.OnMouseOut; - this.focusFunction = this.OnFocus; + this.onMoveFunction = this.OnAddonMove; + this.onMouseOverFunction = this.OnAddonMouseOver; + this.onMouseOutFunction = this.OnAddonMouseOut; + this.focusFunction = this.OnAddonFocus; // Overwrite specific virtual table entries this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); @@ -409,7 +409,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); } - private void OnMove(AtkUnitBase* thisPtr) + private void OnAddonMove(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); @@ -422,13 +422,13 @@ internal unsafe class AddonVirtualTable : IDisposable } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnMove. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception when calling original OnAddonMove. This may be a bug in the game or another plugin hooking this method."); } this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs); } - private void OnMouseOver(AtkUnitBase* thisPtr) + private void OnAddonMouseOver(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); @@ -441,13 +441,13 @@ internal unsafe class AddonVirtualTable : IDisposable } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnMouseOver. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception when calling original OnAddonMouseOver. This may be a bug in the game or another plugin hooking this method."); } this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs); } - private void OnMouseOut(AtkUnitBase* thisPtr) + private void OnAddonMouseOut(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); @@ -460,13 +460,13 @@ internal unsafe class AddonVirtualTable : IDisposable } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnMouseOut. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception when calling original OnAddonMouseOut. This may be a bug in the game or another plugin hooking this method."); } this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs); } - private void OnFocus(AtkUnitBase* thisPtr) + private void OnAddonFocus(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); @@ -479,7 +479,7 @@ internal unsafe class AddonVirtualTable : IDisposable } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnFocus. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception when calling original OnAddonFocus. This may be a bug in the game or another plugin hooking this method."); } this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs); From fb229a0a128dd36b3e531be7bd00d260649ce379 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 1 Dec 2025 12:07:34 +0100 Subject: [PATCH 248/477] Fix PlayerState.Level being synced --- Dalamud/Game/Player/PlayerState.cs | 2 +- Dalamud/Plugin/Services/IPlayerState.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Player/PlayerState.cs b/Dalamud/Game/Player/PlayerState.cs index 917c946db..bd19b5bfb 100644 --- a/Dalamud/Game/Player/PlayerState.cs +++ b/Dalamud/Game/Player/PlayerState.cs @@ -77,7 +77,7 @@ internal unsafe class PlayerState : IServiceType, IPlayerState public RowRef ClassJob => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->CurrentClassJobId) : default; /// - public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default; + public short Level => this.IsLoaded && this.ClassJob.IsValid ? this.GetClassJobLevel(this.ClassJob.Value) : this.EffectiveLevel; /// public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced; diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index 1416dfb77..21d88010b 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -79,7 +79,7 @@ public interface IPlayerState : IDalamudService bool IsLevelSynced { get; } /// - /// Gets the effective level of the local character. + /// Gets the effective level of the local character, taking level sync into account. /// short EffectiveLevel { get; } From 14e97a1a374b7968f28dc856203e9ba990c31ccd Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Mon, 1 Dec 2025 14:19:12 -0800 Subject: [PATCH 249/477] Use local variable to track pushed style state --- Dalamud/Interface/Windowing/Window.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 700481ce5..e90e38119 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -57,7 +57,6 @@ public abstract class Window private bool hasError = false; private Exception? lastError; - private bool isErrorStylePushed; /// /// Initializes a new instance of the class. @@ -426,6 +425,7 @@ public abstract class Window UIGlobals.PlaySoundEffect(this.OnOpenSfxId); } + var isErrorStylePushed = false; if (!this.hasError) { this.PreDraw(); @@ -434,7 +434,7 @@ public abstract class Window else { Style.StyleModelV1.DalamudStandard.Push(); - this.isErrorStylePushed = true; + isErrorStylePushed = true; } if (this.ForceMainWindow) @@ -697,7 +697,7 @@ public abstract class Window } else { - if (this.isErrorStylePushed) + if (isErrorStylePushed) { Style.StyleModelV1.DalamudStandard.Pop(); } From 518b3a4fb351eb1a826d3712f1c7fc4b712d658f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 3 Dec 2025 16:43:12 +0100 Subject: [PATCH 250/477] Fix NounProcessor BeastTribe column offset --- Dalamud/Game/Text/Noun/NounParams.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Text/Noun/NounParams.cs b/Dalamud/Game/Text/Noun/NounParams.cs index 3d5c424be..ab7a732d2 100644 --- a/Dalamud/Game/Text/Noun/NounParams.cs +++ b/Dalamud/Game/Text/Noun/NounParams.cs @@ -60,8 +60,8 @@ internal record struct NounParams() /// public readonly int ColumnOffset => this.SheetName switch { - // See "E8 ?? ?? ?? ?? 44 8B 6B 08" - nameof(LSheets.BeastTribe) => 10, + // See "E8 ?? ?? ?? ?? 44 8B 66 ?? 8B E8" + nameof(LSheets.BeastTribe) => 11, nameof(LSheets.DeepDungeonItem) => 1, nameof(LSheets.DeepDungeonEquipment) => 1, nameof(LSheets.DeepDungeonMagicStone) => 1, From f198ce46dc1d2100d95d4c090e446077e2af5729 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 3 Dec 2025 16:47:13 +0100 Subject: [PATCH 251/477] Add self tests for ColumnOffset --- .../Steps/NounProcessorSelfTestStep.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs index ccb23d395..ccccc691c 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NounProcessorSelfTestStep.cs @@ -191,6 +191,29 @@ internal class NounProcessorSelfTestStep : ISelfTestStep new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveFirstPerson, 1, "mes mémoquartz inhabituels fantasmagoriques"), new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveSecondPerson, 1, "tes mémoquartz inhabituels fantasmagoriques"), new(nameof(LSheets.Item), 44348, ClientLanguage.French, 2, (int)FrenchArticleType.PossessiveThirdPerson, 1, "ses mémoquartz inhabituels fantasmagoriques"), + + // ColumnOffset tests + + new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a Amalj'aa"), + new(nameof(LSheets.BeastTribe), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Amalj'aa"), + + new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an aetherpool arm"), + new(nameof(LSheets.DeepDungeonEquipment), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the aetherpool arm"), + + new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pomander of safety"), + new(nameof(LSheets.DeepDungeonItem), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pomander of safety"), + + new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a splinter of Inferno magicite"), + new(nameof(LSheets.DeepDungeonMagicStone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the splinter of Inferno magicite"), + + new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "an Unei demiclone"), + new(nameof(LSheets.DeepDungeonDemiclone), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the Unei demiclone"), + + new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a pair of oval spectacles"), + new(nameof(LSheets.Glasses), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the pair of oval spectacles"), + + new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Indefinite, 1, "a shaded spectacles"), + new(nameof(LSheets.GlassesStyle), 1, ClientLanguage.English, 1, (int)EnglishArticleType.Definite, 1, "the shaded spectacles"), ]; private enum GermanCases From 0e6dae9f6476050eaca0e01d2560bbbb136b123d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Dec 2025 18:39:04 +0000 Subject: [PATCH 252/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index e5f586630..e5dedba42 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e5f586630ef06fa48d5dc0d8c0fa679323093c77 +Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f From df0bfc18c3877c027000b5400e78359e8b21b9f0 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 4 Dec 2025 01:10:51 +0100 Subject: [PATCH 253/477] Make ImGuiHelpers.CreateDrawData() internal for now --- Dalamud/Interface/Utility/ImGuiHelpers.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index b8e7d5fe3..ee2840d3d 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -234,15 +234,6 @@ public static partial class ImGuiHelpers ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) => Service.Get().CompileAndDrawWrapped(text, style, imGuiId, buttonFlags); - /// Creates a draw data that will draw the given SeString onto it. - /// SeString to render. - /// Initial rendering style. - /// A new self-contained draw data. - public static BufferBackedImDrawData CreateDrawData( - ReadOnlySpan sss, - scoped in SeStringDrawParams style = default) => - Service.Get().CreateDrawData(sss, style); - /// /// Write unformatted text wrapped. /// @@ -584,6 +575,15 @@ public static partial class ImGuiHelpers public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => self.IsNull ? other : self; + /// Creates a draw data that will draw the given SeString onto it. + /// SeString to render. + /// Initial rendering style. + /// A new self-contained draw data. + internal static BufferBackedImDrawData CreateDrawData( + ReadOnlySpan sss, + scoped in SeStringDrawParams style = default) => + Service.Get().CreateDrawData(sss, style); + /// /// Mark 4K page as used, after adding a codepoint to a font. /// From 1fe2d5412839378a446fdf2aea867eb66d731c78 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 4 Dec 2025 01:29:04 +0100 Subject: [PATCH 254/477] Upgrade cimgui, prep for viewport alpha --- lib/cimgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cimgui b/lib/cimgui index 27c8565f6..bc3272967 160000 --- a/lib/cimgui +++ b/lib/cimgui @@ -1 +1 @@ -Subproject commit 27c8565f631b004c3266373890e41ecc627f775b +Subproject commit bc327296758d57d3bdc963cb6ce71dd5b0c7e54c From 9bce0d33a6deb5b1c8ca22cd53389a97c459dfdf Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 4 Dec 2025 02:04:27 +0100 Subject: [PATCH 255/477] Don't try to free CLR memory --- .../ImGuiSeStringRenderer/Internal/SeStringRenderer.cs | 2 +- .../Interface/ImGuiSeStringRenderer/SeStringDrawState.cs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index 0099e6e5d..87df2da2c 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -168,7 +168,7 @@ internal class SeStringRenderer : IServiceType // This also does argument validation for drawParams. Do it here. // `using var` makes a struct read-only, but we do want to modify it. - using var stateStorage = new SeStringDrawState( + var stateStorage = new SeStringDrawState( sss, drawParams, ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes), diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 722de1fda..11c1120b4 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -17,7 +17,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer; /// Calculated values from using ImGui styles. [StructLayout(LayoutKind.Sequential)] -public unsafe ref struct SeStringDrawState : IDisposable +public unsafe ref struct SeStringDrawState { private static readonly int ChannelCount = Enum.GetValues().Length; @@ -181,10 +181,6 @@ public unsafe ref struct SeStringDrawState : IDisposable /// Gets the text fragments. internal List Fragments { get; } - /// - public void Dispose() => - ImGuiNative.Destroy((ImDrawListSplitter*)Unsafe.AsPointer(ref this.splitter)); - /// Sets the current channel in the ImGui draw list splitter. /// Channel to switch to. [MethodImpl(MethodImplOptions.AggressiveInlining)] From 1b5fbaa82ed43343f73dfcf72a5941ba0ca16d60 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 4 Dec 2025 02:04:45 +0100 Subject: [PATCH 256/477] Access custom font atlas fields directly through bindings --- .../Utility/BufferBackedImDrawData.cs | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs index 112fda8a8..e6128992a 100644 --- a/Dalamud/Interface/Utility/BufferBackedImDrawData.cs +++ b/Dalamud/Interface/Utility/BufferBackedImDrawData.cs @@ -39,9 +39,8 @@ public unsafe struct BufferBackedImDrawData : IDisposable *ds = default; var atlas = ImGui.GetIO().Fonts; - ref var atlasTail = ref ImFontAtlasTailReal.From(atlas); ds->SharedData = *ImGui.GetDrawListSharedData().Handle; - ds->SharedData.TexIdCommon = atlas.Textures[atlasTail.TextureIndexCommon].TexID; + ds->SharedData.TexIdCommon = atlas.Textures[atlas.TextureIndexCommon].TexID; ds->SharedData.TexUvWhitePixel = atlas.TexUvWhitePixel; ds->SharedData.TexUvLines = (Vector4*)Unsafe.AsPointer(ref atlas.TexUvLines[0]); ds->SharedData.Font = ImGui.GetIO().FontDefault; @@ -60,7 +59,7 @@ public unsafe struct BufferBackedImDrawData : IDisposable res.ListPtr._ResetForNewFrame(); res.ListPtr.PushClipRectFullScreen(); - res.ListPtr.PushTextureID(new(atlasTail.TextureIndexCommon)); + res.ListPtr.PushTextureID(new(atlas.TextureIndexCommon)); return res; } @@ -90,23 +89,4 @@ public unsafe struct BufferBackedImDrawData : IDisposable public ImDrawList List; public ImDrawListSharedData SharedData; } - - [StructLayout(LayoutKind.Sequential)] - private struct ImFontAtlasTailReal - { - /// Index of texture containing the below. - public int TextureIndexCommon; - - /// Custom texture rectangle ID for both of the below. - public int PackIdCommon; - - /// Custom texture rectangle for white pixel and mouse cursors. - public ImFontAtlasCustomRect RectMouseCursors; - - /// Custom texture rectangle for baked anti-aliased lines. - public ImFontAtlasCustomRect RectLines; - - public static ref ImFontAtlasTailReal From(ImFontAtlasPtr fontAtlasPtr) => - ref *(ImFontAtlasTailReal*)(&fontAtlasPtr.Handle->FontBuilderFlags + sizeof(uint)); - } } From ddc743aae1337f968223ce791a3720d48b0e71b8 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 4 Dec 2025 23:00:36 +0100 Subject: [PATCH 257/477] Note that font ptr must be supplied when setting TargetDrawList --- .../Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs index f3d4c44e9..1d8126f3b 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs @@ -12,7 +12,10 @@ public record struct SeStringDrawParams /// Gets or sets the target draw list. /// Target draw list, default(ImDrawListPtr) to not draw, or null to use /// (the default). - /// If this value is set, will not be called, and ImGui ID will be ignored. + /// + /// If this value is set, will not be called, and ImGui ID will be ignored. + /// You must specify a valid draw list and a valid font via if you set this value, + /// since the renderer will not be able to retrieve them from ImGui context. /// public ImDrawListPtr? TargetDrawList { get; set; } From 0112e17fdb052d0e3ebd6dc87b9b8bfaeaf9e1e0 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 4 Dec 2025 23:27:06 +0100 Subject: [PATCH 258/477] Replace internal SharpDX usage with TerraFX --- .../Internals/FontAtlasFactory.BuildToolkit.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 2a93cf093..41c87fd39 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -15,7 +15,6 @@ using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Storage.Assets; using Dalamud.Utility; -using SharpDX.DXGI; using TerraFX.Interop.DirectX; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -749,7 +748,7 @@ internal sealed partial class FontAtlasFactory new( width, height, - (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm), + (int)(use4 ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM), width * bpp), buf, name); From da7be64fdf3bfd69cd69d77b98d1a7eaf2f3a73a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 4 Dec 2025 23:31:31 +0100 Subject: [PATCH 259/477] Remove SharpDX --- Dalamud/Dalamud.csproj | 2 - Dalamud/Interface/UiBuilder.cs | 16 ------ Dalamud/Storage/Assets/DalamudAssetPurpose.cs | 6 +-- Dalamud/Utility/VectorExtensions.cs | 51 ------------------- Directory.Packages.props | 2 - 5 files changed, 3 insertions(+), 74 deletions(-) delete mode 100644 Dalamud/Utility/VectorExtensions.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index b9b453f89..e8c2516af 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -73,8 +73,6 @@ all - - diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index e38537018..6e4740b22 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -12,7 +12,6 @@ using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.Internal; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; -using Dalamud.Plugin; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Serilog; @@ -150,13 +149,6 @@ public interface IUiBuilder /// public ImFontPtr FontMono { get; } - /// - /// Gets the game's active Direct3D device. - /// - // TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud. - [Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")] - SharpDX.Direct3D11.Device Device { get; } - /// Gets the game's active Direct3D device. /// Pointer to the instance of IUnknown that the game is using and should be containing an ID3D11Device, /// or 0 if it is not available yet. @@ -302,8 +294,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder private IFontHandle? monoFontHandle; private IFontHandle? iconFontFixedWidthHandle; - private SharpDX.Direct3D11.Device? sdxDevice; - /// /// Initializes a new instance of the class and registers it. /// You do not have to call this manually. @@ -493,12 +483,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder this.InterfaceManagerWithScene?.MonoFontHandle ?? throw new InvalidOperationException("Scene is not yet ready."))); - /// - // TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud. - [Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")] - public SharpDX.Direct3D11.Device Device => - this.sdxDevice ??= new(this.InterfaceManagerWithScene!.Backend!.DeviceHandle); - /// public nint DeviceHandle => this.InterfaceManagerWithScene?.Backend?.DeviceHandle ?? 0; diff --git a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs index e6c7bd920..69de1f871 100644 --- a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs +++ b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs @@ -11,12 +11,12 @@ public enum DalamudAssetPurpose Empty = 0, /// - /// The asset is a .png file, and can be purposed as a . + /// The asset is a .png file, and can be purposed as a . /// TextureFromPng = 10, - + /// - /// The asset is a raw texture, and can be purposed as a . + /// The asset is a raw texture, and can be purposed as a . /// TextureFromRaw = 1001, diff --git a/Dalamud/Utility/VectorExtensions.cs b/Dalamud/Utility/VectorExtensions.cs deleted file mode 100644 index f617c8420..000000000 --- a/Dalamud/Utility/VectorExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Numerics; - -namespace Dalamud.Utility; - -/// -/// Extension methods for System.Numerics.VectorN and SharpDX.VectorN. -/// -public static class VectorExtensions -{ - /// - /// Converts a SharpDX vector to System.Numerics. - /// - /// Vector to convert. - /// A converted vector. - public static Vector2 ToSystem(this SharpDX.Vector2 vec) => new(x: vec.X, y: vec.Y); - - /// - /// Converts a SharpDX vector to System.Numerics. - /// - /// Vector to convert. - /// A converted vector. - public static Vector3 ToSystem(this SharpDX.Vector3 vec) => new(x: vec.X, y: vec.Y, z: vec.Z); - - /// - /// Converts a SharpDX vector to System.Numerics. - /// - /// Vector to convert. - /// A converted vector. - public static Vector4 ToSystem(this SharpDX.Vector4 vec) => new(x: vec.X, y: vec.Y, z: vec.Z, w: vec.W); - - /// - /// Converts a System.Numerics vector to SharpDX. - /// - /// Vector to convert. - /// A converted vector. - public static SharpDX.Vector2 ToSharpDX(this Vector2 vec) => new(x: vec.X, y: vec.Y); - - /// - /// Converts a System.Numerics vector to SharpDX. - /// - /// Vector to convert. - /// A converted vector. - public static SharpDX.Vector3 ToSharpDX(this Vector3 vec) => new(x: vec.X, y: vec.Y, z: vec.Z); - - /// - /// Converts a System.Numerics vector to SharpDX. - /// - /// Vector to convert. - /// A converted vector. - public static SharpDX.Vector4 ToSharpDX(this Vector4 vec) => new(x: vec.X, y: vec.Y, z: vec.Z, w: vec.W); -} diff --git a/Directory.Packages.props b/Directory.Packages.props index 903a8ee88..481e7591d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,8 +27,6 @@ - - From ddc31132444f350a8bb1f794a8d5e91bec00c9a3 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Dec 2025 00:37:25 +0100 Subject: [PATCH 260/477] Update TerraFX.Interop.Windows --- .../IObjectWithLocalizableName.cs | 4 +-- .../FontIdentifier/SystemFontFamilyId.cs | 4 +-- .../Interface/FontIdentifier/SystemFontId.cs | 6 ++-- .../ImGuiBackend/Helpers/ReShadePeeler.cs | 14 ++++---- .../InputHandler/Win32InputHandler.cs | 10 +++--- .../Interface/Internal/InterfaceManager.cs | 2 +- .../ReShadeAddonInterface.Exports.cs | 6 ++-- .../ReShadeHandling/ReShadeUnwrapper.cs | 2 +- .../Interface/Internal/StaThreadService.cs | 8 ++--- .../Textures/Internal/BitmapCodecInfo.cs | 4 +-- .../Internal/TextureManager.BlameTracker.cs | 6 ++-- .../Internal/TextureManager.Clipboard.cs | 6 ++-- Dalamud/Service/LoadingDialog.cs | 32 +++++++++---------- Dalamud/Utility/ClipboardFormats.cs | 4 +-- Dalamud/Utility/TerraFxCom/ManagedIStream.cs | 28 ++++++++-------- .../TerraFxComInterfaceExtensions.cs | 8 ++--- Directory.Packages.props | 2 +- 17 files changed, 73 insertions(+), 73 deletions(-) diff --git a/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs index 2b970a5fd..4b3860431 100644 --- a/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs +++ b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs @@ -64,9 +64,9 @@ public interface IObjectWithLocalizableName var result = new Dictionary((int)count); for (var i = 0u; i < count; i++) { - fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError(); + fn->GetLocaleName(i, buf, maxStrLen).ThrowOnError(); var key = new string(buf); - fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError(); + fn->GetString(i, buf, maxStrLen).ThrowOnError(); var value = new string(buf); result[key.ToLowerInvariant()] = value; } diff --git a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs index 420ee77a4..83a5e810d 100644 --- a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs +++ b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs @@ -133,8 +133,8 @@ public sealed class SystemFontFamilyId : IFontFamilyId var familyIndex = 0u; BOOL exists = false; - fixed (void* pName = this.EnglishName) - sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError(); + fixed (char* pName = this.EnglishName) + sfc.Get()->FindFamilyName(pName, &familyIndex, &exists).ThrowOnError(); if (!exists) throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found."); diff --git a/Dalamud/Interface/FontIdentifier/SystemFontId.cs b/Dalamud/Interface/FontIdentifier/SystemFontId.cs index e11759a88..8401f4c79 100644 --- a/Dalamud/Interface/FontIdentifier/SystemFontId.cs +++ b/Dalamud/Interface/FontIdentifier/SystemFontId.cs @@ -113,8 +113,8 @@ public sealed class SystemFontId : IFontId var familyIndex = 0u; BOOL exists = false; - fixed (void* name = this.Family.EnglishName) - sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError(); + fixed (char* name = this.Family.EnglishName) + sfc.Get()->FindFamilyName(name, &familyIndex, &exists).ThrowOnError(); if (!exists) throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found."); @@ -151,7 +151,7 @@ public sealed class SystemFontId : IFontId flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError(); var path = stackalloc char[(int)pathSize + 1]; - flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError(); + flocal.Get()->GetFilePathFromKey(refKey, refKeySize, path, pathSize + 1).ThrowOnError(); return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex()); } diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs index 824ba382a..3f3c98c26 100644 --- a/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs +++ b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs @@ -104,19 +104,19 @@ internal static unsafe class ReShadePeeler fixed (byte* pfn5 = "glBegin"u8) fixed (byte* pfn6 = "vkCreateDevice"u8) { - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == null) continue; } diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs index 596df4c67..18330d3a2 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs @@ -622,7 +622,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND), lpfnWndProc = (delegate* unmanaged)Marshal .GetFunctionPointerForDelegate(this.input.wndProcDelegate), - lpszClassName = (ushort*)windowClassNamePtr, + lpszClassName = windowClassNamePtr, }; if (RegisterClassExW(&wcex) == 0) @@ -658,7 +658,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler fixed (char* windowClassNamePtr = WindowClassName) { UnregisterClassW( - (ushort*)windowClassNamePtr, + windowClassNamePtr, (HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module)); } @@ -781,8 +781,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler { data->Hwnd = CreateWindowExW( (uint)data->DwExStyle, - (ushort*)windowClassNamePtr, - (ushort*)windowClassNamePtr, + windowClassNamePtr, + windowClassNamePtr, (uint)data->DwStyle, rect.left, rect.top, @@ -993,7 +993,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler { var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; fixed (char* pwszTitle = MemoryHelper.ReadStringNullTerminated((nint)title)) - SetWindowTextW(data->Hwnd, (ushort*)pwszTitle); + SetWindowTextW(data->Hwnd, pwszTitle); } [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 76a1b5172..96fcb7dfd 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -256,7 +256,7 @@ internal partial class InterfaceManager : IInternalDisposableService var gwh = default(HWND); fixed (char* pClass = "FFXIVGAME") { - while ((gwh = FindWindowExW(default, gwh, (ushort*)pClass, default)) != default) + while ((gwh = FindWindowExW(default, gwh, pClass, default)) != default) { uint pid; _ = GetWindowThreadProcessId(gwh, &pid); diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs index d8d210076..d7d3b56c3 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs @@ -63,11 +63,11 @@ internal sealed unsafe partial class ReShadeAddonInterface return; - bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res) + static bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res) { Span name8 = stackalloc byte[Encoding.UTF8.GetByteCount(name) + 1]; name8[Encoding.UTF8.GetBytes(name, name8)] = 0; - *(nint*)res = GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0])); + *(nint*)res = (nint)GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0])); return *(nint*)res != 0; } } @@ -174,7 +174,7 @@ internal sealed unsafe partial class ReShadeAddonInterface CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE, CERT.CERT_NAME_ISSUER_FLAG, null, - (ushort*)Unsafe.AsPointer(ref issuerName[0]), + (char*)Unsafe.AsPointer(ref issuerName[0]), pcb); if (pcb == 0) throw new Win32Exception("CertGetNameStringW(2)"); diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs index f1210425d..711de6eb2 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs @@ -94,7 +94,7 @@ internal static unsafe class ReShadeUnwrapper static bool HasProcExported(ProcessModule m, ReadOnlySpan name) { fixed (byte* p = name) - return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != 0; + return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != null; } } diff --git a/Dalamud/Interface/Internal/StaThreadService.cs b/Dalamud/Interface/Internal/StaThreadService.cs index 87e003288..bb5caa281 100644 --- a/Dalamud/Interface/Internal/StaThreadService.cs +++ b/Dalamud/Interface/Internal/StaThreadService.cs @@ -216,7 +216,7 @@ internal partial class StaThreadService : IInternalDisposableService lpfnWndProc = &MessageReceiverWndProcStatic, hInstance = hInstance, hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1), - lpszClassName = (ushort*)name, + lpszClassName = name, }; wndClassAtom = RegisterClassExW(&wndClass); @@ -226,8 +226,8 @@ internal partial class StaThreadService : IInternalDisposableService this.messageReceiverHwndTask.SetResult( CreateWindowExW( 0, - (ushort*)wndClassAtom, - (ushort*)name, + (char*)wndClassAtom, + name, 0, CW_USEDEFAULT, CW_USEDEFAULT, @@ -275,7 +275,7 @@ internal partial class StaThreadService : IInternalDisposableService _ = OleFlushClipboard(); OleUninitialize(); if (wndClassAtom != 0) - UnregisterClassW((ushort*)wndClassAtom, hInstance); + UnregisterClassW((char*)wndClassAtom, hInstance); this.messageReceiverHwndTask.TrySetException(e); } } diff --git a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs index 3d5456500..ec56caadd 100644 --- a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs +++ b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs @@ -44,12 +44,12 @@ internal sealed class BitmapCodecInfo : IBitmapCodecInfo private static unsafe string ReadStringUsing( IWICBitmapCodecInfo* codecInfo, - delegate* unmanaged readFuncPtr) + delegate* unmanaged[MemberFunction] readFuncPtr) { var cch = 0u; _ = readFuncPtr(codecInfo, 0, null, &cch); var buf = stackalloc char[(int)cch + 1]; - Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, (ushort*)buf, &cch)); + Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, buf, &cch)); return new(buf, 0, (int)cch); } } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs index 837b41271..fde40d462 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs @@ -219,14 +219,14 @@ internal sealed partial class TextureManager return; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int QueryInterfaceStatic(IUnknown* pThis, Guid* riid, void** ppvObject) => ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static uint AddRefStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static uint ReleaseStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs index 8a510e967..75f7ab975 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs @@ -133,7 +133,7 @@ internal sealed partial class TextureManager }, }, }; - namea.AsSpan().CopyTo(new(fgda.fgd.e0.cFileName, 260)); + namea.AsSpan().CopyTo(new(Unsafe.AsPointer(ref fgda.fgd.e0.cFileName[0]), 260)); AddToDataObject( pdo, @@ -157,7 +157,7 @@ internal sealed partial class TextureManager }, }, }; - preferredFileNameWithoutExtension.AsSpan().CopyTo(new(fgdw.fgd.e0.cFileName, 260)); + preferredFileNameWithoutExtension.AsSpan().CopyTo(new(Unsafe.AsPointer(ref fgdw.fgd.e0.cFileName[0]), 260)); AddToDataObject( pdo, @@ -450,7 +450,7 @@ internal sealed partial class TextureManager try { IStream* pfs; - SHCreateStreamOnFileW((ushort*)pPath, sharedRead, &pfs).ThrowOnError(); + SHCreateStreamOnFileW((char*)pPath, sharedRead, &pfs).ThrowOnError(); var stgm2 = new STGMEDIUM { diff --git a/Dalamud/Service/LoadingDialog.cs b/Dalamud/Service/LoadingDialog.cs index 424087743..ea45d3bb2 100644 --- a/Dalamud/Service/LoadingDialog.cs +++ b/Dalamud/Service/LoadingDialog.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Drawing; @@ -294,18 +294,18 @@ internal sealed class LoadingDialog ? null : Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe")); - fixed (void* pszEmpty = "-") - fixed (void* pszWindowTitle = "Dalamud") - fixed (void* pszDalamudBoot = "Dalamud.Boot.dll") - fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES") - fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide")) - fixed (void* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs")) - fixed (void* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs")) + fixed (char* pszEmpty = "-") + fixed (char* pszWindowTitle = "Dalamud") + fixed (char* pszDalamudBoot = "Dalamud.Boot.dll") + fixed (char* pszThemesManifestResourceName = "RT_MANIFEST_THEMES") + fixed (char* pszHide = Loc.Localize("LoadingDialogHide", "Hide")) + fixed (char* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs")) + fixed (char* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs")) { var taskDialogButton = new TASKDIALOG_BUTTON { nButtonID = IDOK, - pszButtonText = (ushort*)pszHide, + pszButtonText = pszHide, }; var taskDialogConfig = new TASKDIALOGCONFIG { @@ -318,8 +318,8 @@ internal sealed class LoadingDialog (int)TDF_CALLBACK_TIMER | (extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN), dwCommonButtons = 0, - pszWindowTitle = (ushort*)pszWindowTitle, - pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (ushort*)extractedIcon.Handle, + pszWindowTitle = pszWindowTitle, + pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (char*)extractedIcon.Handle, pszMainInstruction = null, pszContent = null, cButtons = 1, @@ -329,9 +329,9 @@ internal sealed class LoadingDialog pRadioButtons = null, nDefaultRadioButton = 0, pszVerificationText = null, - pszExpandedInformation = (ushort*)pszEmpty, - pszExpandedControlText = (ushort*)pszShowLatestLogs, - pszCollapsedControlText = (ushort*)pszHideLatestLogs, + pszExpandedInformation = pszEmpty, + pszExpandedControlText = pszShowLatestLogs, + pszCollapsedControlText = pszHideLatestLogs, pszFooterIcon = null, pszFooter = null, pfCallback = &HResultFuncBinder, @@ -348,8 +348,8 @@ internal sealed class LoadingDialog { cbSize = (uint)sizeof(ACTCTXW), dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID, - lpResourceName = (ushort*)pszThemesManifestResourceName, - hModule = GetModuleHandleW((ushort*)pszDalamudBoot), + lpResourceName = pszThemesManifestResourceName, + hModule = GetModuleHandleW(pszDalamudBoot), }; hActCtx = CreateActCtxW(&actctx); if (hActCtx == default) diff --git a/Dalamud/Utility/ClipboardFormats.cs b/Dalamud/Utility/ClipboardFormats.cs index 07b6c00d6..b80e05dd3 100644 --- a/Dalamud/Utility/ClipboardFormats.cs +++ b/Dalamud/Utility/ClipboardFormats.cs @@ -30,8 +30,8 @@ internal static class ClipboardFormats private static unsafe uint ClipboardFormatFromName(ReadOnlySpan name) { uint cf; - fixed (void* p = name) - cf = RegisterClipboardFormatW((ushort*)p); + fixed (char* p = name) + cf = RegisterClipboardFormatW(p); if (cf != 0) return cf; throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? diff --git a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs index caec65da2..eb1997daf 100644 --- a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs +++ b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs @@ -57,60 +57,60 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable static ManagedIStream? ToManagedObject(void* pThis) => GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as ManagedIStream; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) => ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static uint AddRefStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static uint ReleaseStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) => ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) => ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int SeekStatic( IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) => ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) => ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int CopyToStatic( IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, ULARGE_INTEGER* pcbWritten) => ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int CommitStatic(IStream* pThis, uint grfCommitFlags) => ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int UnlockRegionStatic( IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) => ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_UNEXPECTED; } diff --git a/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs index f9252839f..ec108403e 100644 --- a/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs +++ b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs @@ -88,7 +88,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions fixed (char* pPath = path) { SHCreateStreamOnFileEx( - (ushort*)pPath, + pPath, grfMode, (uint)attributes, fCreate, @@ -115,7 +115,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions { fixed (char* pName = name) { - var option = new PROPBAG2 { pstrName = (ushort*)pName }; + var option = new PROPBAG2 { pstrName = pName }; return obj.Write(1, &option, &varValue); } } @@ -145,7 +145,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions try { fixed (char* pName = name) - return obj.SetMetadataByName((ushort*)pName, &propVarValue); + return obj.SetMetadataByName(pName, &propVarValue); } finally { @@ -165,7 +165,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name) { fixed (char* pName = name) - return obj.RemoveMetadataByName((ushort*)pName); + return obj.RemoveMetadataByName(pName); } [LibraryImport("propsys.dll")] diff --git a/Directory.Packages.props b/Directory.Packages.props index 903a8ee88..d62d247c3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From fc983458fa16c698977b386fdefac4d385256f01 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Dec 2025 01:44:18 +0100 Subject: [PATCH 261/477] Update Nuke --- build/DalamudBuild.cs | 6 ++---- build/build.csproj | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs index ba2b09a4d..1a189f2c7 100644 --- a/build/DalamudBuild.cs +++ b/build/DalamudBuild.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using Nuke.Common; using Nuke.Common.Execution; using Nuke.Common.Git; @@ -128,7 +127,7 @@ public class DalamudBuild : NukeBuild if (IsCIBuild) { s = s - .SetProcessArgumentConfigurator(a => a.Add("/clp:NoSummary")); // Disable MSBuild summary on CI builds + .SetProcessAdditionalArguments("/clp:NoSummary"); // Disable MSBuild summary on CI builds } // We need to emit compiler generated files for the docs build, since docfx can't run generators directly // TODO: This fails every build after this because of redefinitions... @@ -238,7 +237,6 @@ public class DalamudBuild : NukeBuild .SetProject(InjectorProjectFile) .SetConfiguration(Configuration)); - FileSystemTasks.DeleteDirectory(ArtifactsDirectory); - Directory.CreateDirectory(ArtifactsDirectory); + ArtifactsDirectory.CreateOrCleanDirectory(); }); } diff --git a/build/build.csproj b/build/build.csproj index 1e1416d92..7096c7f8a 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -11,7 +11,7 @@ false - + From e7d4786a1fec6411908ed9e319f1a06b67738389 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:18:57 +0100 Subject: [PATCH 262/477] Oops, wrong version --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 481e7591d..6c5070d35 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From 7cf20fe102bf14bea99dead9ec0d4e5ad3c23c92 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Dec 2025 00:35:52 +0100 Subject: [PATCH 263/477] Update Microsoft.Windows.CsWin32 --- Dalamud/SafeMemory.cs | 15 +++++++++++++-- Dalamud/Utility/FilesystemUtil.cs | 10 +++++++--- Directory.Packages.props | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs index a8ac40a5d..9a1af0625 100644 --- a/Dalamud/SafeMemory.cs +++ b/Dalamud/SafeMemory.cs @@ -1,6 +1,8 @@ using System.Runtime.InteropServices; using System.Text; +using Windows.Win32.Foundation; + namespace Dalamud; /// @@ -28,12 +30,18 @@ public static class SafeMemory /// Whether the read succeeded. public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer) { + if (Handle.IsClosed || Handle.IsInvalid) + { + buffer = []; + return false; + } + buffer = new byte[count <= 0 ? 0 : count]; fixed (byte* p = buffer) { UIntPtr bytesRead; if (!Windows.Win32.PInvoke.ReadProcessMemory( - Handle, + (HANDLE)Handle.DangerousGetHandle(), address.ToPointer(), p, new UIntPtr((uint)count), @@ -54,6 +62,9 @@ public static class SafeMemory /// Whether the write succeeded. public static unsafe bool WriteBytes(IntPtr address, byte[] buffer) { + if (Handle.IsClosed || Handle.IsInvalid) + return false; + if (buffer.Length == 0) return true; @@ -61,7 +72,7 @@ public static class SafeMemory fixed (byte* p = buffer) { if (!Windows.Win32.PInvoke.WriteProcessMemory( - Handle, + (HANDLE)Handle.DangerousGetHandle(), address.ToPointer(), p, new UIntPtr((uint)buffer.Length), diff --git a/Dalamud/Utility/FilesystemUtil.cs b/Dalamud/Utility/FilesystemUtil.cs index 3b4298b37..560e06da3 100644 --- a/Dalamud/Utility/FilesystemUtil.cs +++ b/Dalamud/Utility/FilesystemUtil.cs @@ -1,7 +1,8 @@ -using System.ComponentModel; +using System.ComponentModel; using System.IO; using System.Text; +using Windows.Win32.Foundation; using Windows.Win32.Storage.FileSystem; namespace Dalamud.Utility; @@ -61,8 +62,11 @@ public static class FilesystemUtil // Write the data uint bytesWritten = 0; - if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan(bytes), &bytesWritten, null)) - throw new Win32Exception(); + fixed (byte* ptr = bytes) + { + if (!Windows.Win32.PInvoke.WriteFile((HANDLE)tempFile.DangerousGetHandle(), ptr, (uint)bytes.Length, &bytesWritten, null)) + throw new Win32Exception(); + } if (bytesWritten != bytes.Length) throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); diff --git a/Directory.Packages.props b/Directory.Packages.props index 6c5070d35..58e355400 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,7 +27,7 @@ - + From d94cacaac3bec7e2b64d3d13bde9d1dbd61d0714 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Dec 2025 19:10:31 +0100 Subject: [PATCH 264/477] Disable SafeHandles --- Dalamud/EntryPoint.cs | 2 +- Dalamud/Hooking/Hook.cs | 14 +++++++------- Dalamud/NativeMethods.json | 1 + Dalamud/SafeMemory.cs | 12 ++++++------ Dalamud/Utility/FilesystemUtil.cs | 16 +++++++++++----- Dalamud/Utility/Util.cs | 5 ++--- 6 files changed, 28 insertions(+), 22 deletions(-) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 15077f3d8..b5504b046 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -263,7 +263,7 @@ public sealed class EntryPoint var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb"); var searchPath = $".;{symbolPath}"; - var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle(); + var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess(); // Remove any existing Symbol Handler and Init a new one with our search path added Windows.Win32.PInvoke.SymCleanup(currentProcess); diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index faf4658a5..1cd3ef91d 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -201,19 +201,19 @@ public abstract class Hook : IDalamudHook where T : Delegate if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; - using var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName); - if (moduleHandle.IsInvalid) + var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName); + if (moduleHandle.IsNull) throw new Exception($"Could not get a handle to module {moduleName}"); - var procAddress = (nint)Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName); - if (procAddress == IntPtr.Zero) + var procAddress = Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName); + if (procAddress.IsNull) throw new Exception($"Could not get the address of {moduleName}::{exportName}"); - procAddress = HookManager.FollowJmp(procAddress); + var address = HookManager.FollowJmp(procAddress.Value); if (useMinHook) - return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); + return new MinHookHook(address, detour, Assembly.GetCallingAssembly()); else - return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly()); + return new ReloadedHook(address, detour, Assembly.GetCallingAssembly()); } /// diff --git a/Dalamud/NativeMethods.json b/Dalamud/NativeMethods.json index ffb313dfc..46fd3504f 100644 --- a/Dalamud/NativeMethods.json +++ b/Dalamud/NativeMethods.json @@ -1,4 +1,5 @@ { "$schema": "https://aka.ms/CsWin32.schema.json", + "useSafeHandles": false, "allowMarshaling": false } diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs index 9a1af0625..ca0c8ff92 100644 --- a/Dalamud/SafeMemory.cs +++ b/Dalamud/SafeMemory.cs @@ -14,11 +14,11 @@ namespace Dalamud; /// public static class SafeMemory { - private static readonly SafeHandle Handle; + private static readonly HANDLE Handle; static SafeMemory() { - Handle = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle(); + Handle = Windows.Win32.PInvoke.GetCurrentProcess(); } /// @@ -30,7 +30,7 @@ public static class SafeMemory /// Whether the read succeeded. public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer) { - if (Handle.IsClosed || Handle.IsInvalid) + if (Handle.IsNull) { buffer = []; return false; @@ -41,7 +41,7 @@ public static class SafeMemory { UIntPtr bytesRead; if (!Windows.Win32.PInvoke.ReadProcessMemory( - (HANDLE)Handle.DangerousGetHandle(), + Handle, address.ToPointer(), p, new UIntPtr((uint)count), @@ -62,7 +62,7 @@ public static class SafeMemory /// Whether the write succeeded. public static unsafe bool WriteBytes(IntPtr address, byte[] buffer) { - if (Handle.IsClosed || Handle.IsInvalid) + if (Handle.IsNull) return false; if (buffer.Length == 0) @@ -72,7 +72,7 @@ public static class SafeMemory fixed (byte* p = buffer) { if (!Windows.Win32.PInvoke.WriteProcessMemory( - (HANDLE)Handle.DangerousGetHandle(), + Handle, address.ToPointer(), p, new UIntPtr((uint)buffer.Length), diff --git a/Dalamud/Utility/FilesystemUtil.cs b/Dalamud/Utility/FilesystemUtil.cs index 560e06da3..f1b62ee21 100644 --- a/Dalamud/Utility/FilesystemUtil.cs +++ b/Dalamud/Utility/FilesystemUtil.cs @@ -48,33 +48,39 @@ public static class FilesystemUtil // Open the temp file var tempPath = path + ".tmp"; - using var tempFile = Windows.Win32.PInvoke.CreateFile( + var tempFile = Windows.Win32.PInvoke.CreateFile( tempPath, (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), FILE_SHARE_MODE.FILE_SHARE_NONE, null, FILE_CREATION_DISPOSITION.CREATE_ALWAYS, FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, - null); + HANDLE.Null); - if (tempFile.IsInvalid) + if (tempFile.IsNull) throw new Win32Exception(); // Write the data uint bytesWritten = 0; fixed (byte* ptr = bytes) { - if (!Windows.Win32.PInvoke.WriteFile((HANDLE)tempFile.DangerousGetHandle(), ptr, (uint)bytes.Length, &bytesWritten, null)) + if (!Windows.Win32.PInvoke.WriteFile(tempFile, ptr, (uint)bytes.Length, &bytesWritten, null)) throw new Win32Exception(); } if (bytesWritten != bytes.Length) + { + Windows.Win32.PInvoke.CloseHandle(tempFile); throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); + } if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile)) + { + Windows.Win32.PInvoke.CloseHandle(tempFile); throw new Win32Exception(); + } - tempFile.Close(); + Windows.Win32.PInvoke.CloseHandle(tempFile); if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) throw new Win32Exception(); diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 19610ef64..f50efcf0d 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -858,7 +858,7 @@ public static partial class Util var sizeWithTerminators = pathBytesSize + (pathBytes.Length * 2); var dropFilesSize = sizeof(DROPFILES); - var hGlobal = Win32_PInvoke.GlobalAlloc_SafeHandle( + var hGlobal = Win32_PInvoke.GlobalAlloc( GLOBAL_ALLOC_FLAGS.GHND, // struct size + size of encoded strings + null terminator for each // string + two null terminators for end of list @@ -896,12 +896,11 @@ public static partial class Util { Win32_PInvoke.SetClipboardData( (uint)CLIPBOARD_FORMAT.CF_HDROP, - hGlobal); + (Windows.Win32.Foundation.HANDLE)hGlobal.Value); Win32_PInvoke.CloseClipboard(); return true; } - hGlobal.Dispose(); return false; } From a36e11574b14ea6887cb0f7d2513920ebdd820bf Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 6 Dec 2025 01:10:00 +0100 Subject: [PATCH 265/477] Add git status checks to workflow to see what's dirty --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 299d71e95..f552e446b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,8 +33,12 @@ jobs: ($env:REPO_NAME) >> VERSION ($env:BRANCH) >> VERSION ($env:COMMIT) >> VERSION + - name: git status + run: git status - name: Build and Test Dalamud run: .\build.ps1 ci + - name: git status + run: git status - name: Sign Dalamud if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} env: From 3c7dbf9f81e147e45f7b3541844e95cebcc3a5df Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 5 Dec 2025 16:59:17 -0800 Subject: [PATCH 266/477] Remove AddonEventManagerAddressResolver.cs --- .../Game/Addon/Events/AddonEventManager.cs | 30 ++++++++----------- .../AddonEventManagerAddressResolver.cs | 21 ------------- 2 files changed, 12 insertions(+), 39 deletions(-) delete mode 100644 Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 945197e2b..980404940 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -9,7 +9,6 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Events; @@ -32,25 +31,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService private readonly AddonLifecycleEventListener finalizeEventListener; - private readonly AddonEventManagerAddressResolver address; - private readonly Hook onUpdateCursor; + private readonly Hook onUpdateCursor; private readonly ConcurrentDictionary pluginEventControllers; - private AddonCursorType? cursorOverride; + private AtkCursor.CursorType? cursorOverride; [ServiceManager.ServiceConstructor] - private AddonEventManager(TargetSigScanner sigScanner) + private AddonEventManager() { - this.address = new AddonEventManagerAddressResolver(); - this.address.Setup(sigScanner); - this.pluginEventControllers = new ConcurrentDictionary(); this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController()); this.cursorOverride = null; - this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); + this.onUpdateCursor = Hook.FromAddress(AtkUnitManager.Addresses.UpdateCursor.Value, this.UpdateCursorDetour); this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize); this.addonLifecycle.RegisterListener(this.finalizeEventListener); @@ -58,8 +53,6 @@ internal unsafe class AddonEventManager : IInternalDisposableService this.onUpdateCursor.Enable(); } - private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); - /// void IInternalDisposableService.DisposeService() { @@ -117,7 +110,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService /// Force the game cursor to be the specified cursor. /// /// Which cursor to use. - internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; + internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = (AtkCursor.CursorType)cursor; /// /// Un-forces the game cursor. @@ -168,7 +161,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService } } - private nint UpdateCursorDetour(RaptureAtkModule* module) + private void UpdateCursorDetour(AtkUnitManager* thisPtr) { try { @@ -176,13 +169,14 @@ internal unsafe class AddonEventManager : IInternalDisposableService if (this.cursorOverride is not null && atkStage is not null) { - var cursor = (AddonCursorType)atkStage->AtkCursor.Type; - if (cursor != this.cursorOverride) + ref var atkCursor = ref atkStage->AtkCursor; + + if (atkCursor.Type != this.cursorOverride) { - AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); + atkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); } - return nint.Zero; + return; } } catch (Exception e) @@ -190,7 +184,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService Log.Error(e, "Exception in UpdateCursorDetour."); } - return this.onUpdateCursor!.Original(module); + this.onUpdateCursor!.Original(thisPtr); } } diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs deleted file mode 100644 index 415e1b169..000000000 --- a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Dalamud.Game.Addon.Events; - -/// -/// AddonEventManager memory address resolver. -/// -internal class AddonEventManagerAddressResolver : BaseAddressResolver -{ - /// - /// Gets the address of the AtkModule UpdateCursor method. - /// - public nint UpdateCursor { get; private set; } - - /// - /// Scan for and setup any configured address pointers. - /// - /// The signature scanner to facilitate setup. - protected override void Setup64Bit(ISigScanner scanner) - { - this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS - } -} From 45366efd9fc888bf3908144f248b46da00c5d4ff Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 5 Dec 2025 17:10:58 -0800 Subject: [PATCH 267/477] Remove SigScanner from ctor --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index ddcebe718..716ce1bfb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -29,7 +29,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] - private AddonLifecycle(TargetSigScanner sigScanner) + private AddonLifecycle() { this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); this.onInitializeAddonHook.Enable(); From 2e5c560ed729b7e88e6f036aa9987bca5e6f7be0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 6 Dec 2025 12:48:34 +0000 Subject: [PATCH 268/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index e5dedba42..6f339d8f7 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f +Subproject commit 6f339d8f725fa6922449f7e5c584ca6b8fa2fb19 From 9c2d2b7c1dbe192599748e7320fce3c1cc972072 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 6 Dec 2025 15:07:09 +0100 Subject: [PATCH 269/477] Report CLR errors through DalamudCrashHandler/VEH by hooking ReportEventW --- Dalamud.Boot/dllmain.cpp | 41 ++++++++++++++++++++++++++++++++++++++++ Dalamud.Boot/veh.cpp | 37 ++++++++++++++++++++++++++++-------- Dalamud.Boot/veh.h | 1 + 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 80a16f89a..2af78092d 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -331,6 +331,47 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { logging::I("VEH was disabled manually"); } + // ============================== CLR Reporting =================================== // + + // This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it + // was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now. + // Ideally all of this will go away once they get to it. + static std::shared_ptr> s_hook; + s_hook = std::make_shared>("advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW"); + s_hook->set_detour([hook = s_hook.get()]( HANDLE hEventLog, + WORD wType, + WORD wCategory, + DWORD dwEventID, + PSID lpUserSid, + WORD wNumStrings, + DWORD dwDataSize, + LPCWSTR* lpStrings, + LPVOID lpRawData)->BOOL { + + // Check for CLR Error Event IDs + if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception + dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast + dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime + dwEventID != 1027 && // ERT_StackOverflow: The process was terminated due to a stack overflow + dwEventID != 1028) // ERT_CodeContractFailed: The application encountered a bug. A managed code contract (precondition, postcondition, object invariant, or assert) failed + { + return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData); + } + + if (wNumStrings == 0 || lpStrings == nullptr) { + logging::W("ReportEventW called with no strings."); + return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData); + } + + // In most cases, DalamudCrashHandler will kill us now, so call original here to make sure we still write to the event log. + const BOOL original_ret = hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData); + + const std::wstring error_details(lpStrings[0]); + veh::raise_external_event(error_details); + + return original_ret; + }); + // ============================== Dalamud ==================================== // if (static_cast(g_startInfo.BootWaitMessageBox) & static_cast(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint)) diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index b0ec1cefa..24a846e66 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -11,6 +11,8 @@ #include "crashhandler_shared.h" #include "DalamudStartInfo.h" +#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679 + #pragma comment(lib, "comctl32.lib") #if defined _M_IX86 @@ -31,6 +33,8 @@ HANDLE g_crashhandler_process = nullptr; HANDLE g_crashhandler_event = nullptr; HANDLE g_crashhandler_pipe_write = nullptr; +wchar_t g_external_event_info[16384] = L""; + std::recursive_mutex g_exception_handler_mutex; std::chrono::time_point g_time_start; @@ -190,7 +194,11 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS); std::wstring stackTrace; - if (!g_clr) + if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT) + { + stackTrace = std::wstring(g_external_event_info); + } + else if (!g_clr) { stackTrace = L"(no CLR stack trace available)"; } @@ -251,6 +259,12 @@ LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex) LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) { + // special case for CLR exceptions, always trigger crash handler + if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT) + { + return exception_handler(ex); + } + if (ex->ExceptionRecord->ExceptionCode == 0x12345678) { // pass @@ -268,7 +282,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) && !is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip)) - return EXCEPTION_CONTINUE_SEARCH; + return EXCEPTION_CONTINUE_SEARCH; } return exception_handler(ex); @@ -297,7 +311,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536)) { hWritePipe.emplace(hWritePipeRaw, &CloseHandle); - + if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) { hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle); @@ -315,9 +329,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } // additional information - STARTUPINFOEXW siex{}; + STARTUPINFOEXW siex{}; PROCESS_INFORMATION pi{}; - + siex.StartupInfo.cb = sizeof siex; siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE; @@ -385,7 +399,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) argstr.push_back(L' '); } argstr.pop_back(); - + if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr)) { logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError()); @@ -400,7 +414,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) TRUE, // Set handle inheritance to FALSE EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W) nullptr, // Use parent's environment block - nullptr, // Use parent's starting directory + nullptr, // Use parent's starting directory &siex.StartupInfo, // Pointer to STARTUPINFO structure &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) )) @@ -416,7 +430,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } CloseHandle(pi.hThread); - + g_crashhandler_process = pi.hProcess; g_crashhandler_pipe_write = hWritePipe->release(); logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId); @@ -434,3 +448,10 @@ bool veh::remove_handler() } return false; } + +void veh::raise_external_event(const std::wstring& errorMessage) +{ + const auto info_size = std::min(errorMessage.size(), std::size(g_external_event_info) - 1); + wcsncpy_s(g_external_event_info, errorMessage.c_str(), info_size); + RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr); +} diff --git a/Dalamud.Boot/veh.h b/Dalamud.Boot/veh.h index 1905272ea..2a02c374e 100644 --- a/Dalamud.Boot/veh.h +++ b/Dalamud.Boot/veh.h @@ -4,4 +4,5 @@ namespace veh { bool add_handler(bool doFullDump, const std::string& workingDirectory); bool remove_handler(); + void raise_external_event(const std::wstring& info); } From e09c43b8decc55fcbc38cd61102256db0bdc4e29 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 6 Dec 2025 15:07:46 +0100 Subject: [PATCH 270/477] Fix bad exit condition when looping exception records --- DalamudCrashHandler/DalamudCrashHandler.cpp | 78 ++++++++++++--------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index ec7115ffd..107261541 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -119,7 +119,7 @@ std::wstring describe_module(const std::filesystem::path& path) { return std::format(L"", GetLastError()); UINT size = 0; - + std::wstring version = L"v?.?.?.?"; if (LPVOID lpBuffer; VerQueryValueW(block.data(), L"\\", &lpBuffer, &size)) { const auto& v = *static_cast(lpBuffer); @@ -176,7 +176,7 @@ const std::map& get_remote_modules() { std::vector buf(8192); for (size_t i = 0; i < 64; i++) { if (DWORD needed; !EnumProcessModules(g_hProcess, &buf[0], static_cast(std::span(buf).size_bytes()), &needed)) { - std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl; + std::cerr << std::format("EnumProcessModules error: 0x{:x}", GetLastError()) << std::endl; break; } else if (needed > std::span(buf).size_bytes()) { buf.resize(needed / sizeof(HMODULE) + 16); @@ -201,7 +201,7 @@ const std::map& get_remote_modules() { data[hModule] = nth64.OptionalHeader.SizeOfImage; } - + return data; }(); @@ -292,35 +292,43 @@ std::wstring to_address_string(const DWORD64 address, const bool try_ptrderef = void print_exception_info(HANDLE hThread, const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log) { std::vector exRecs; - if (ex.ExceptionRecord) { + if (ex.ExceptionRecord) + { size_t rec_index = 0; size_t read; - exRecs.emplace_back(); + for (auto pRemoteExRec = ex.ExceptionRecord; - pRemoteExRec - && rec_index < 64 - && ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read) - && read >= offsetof(EXCEPTION_RECORD, ExceptionInformation) - && read >= static_cast(reinterpret_cast(&exRecs.back().ExceptionInformation[exRecs.back().NumberParameters]) - reinterpret_cast(&exRecs.back())); - rec_index++) { + pRemoteExRec && rec_index < 64; + rec_index++) + { + exRecs.emplace_back(); + + if (!ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read) + || read < offsetof(EXCEPTION_RECORD, ExceptionInformation) + || read < static_cast(reinterpret_cast(&exRecs.back().ExceptionInformation[exRecs. + back().NumberParameters]) - reinterpret_cast(&exRecs.back()))) + { + exRecs.pop_back(); + break; + } log << std::format(L"\nException Info #{}\n", rec_index); log << std::format(L"Address: {:X}\n", exRecs.back().ExceptionCode); log << std::format(L"Flags: {:X}\n", exRecs.back().ExceptionFlags); log << std::format(L"Address: {:X}\n", reinterpret_cast(exRecs.back().ExceptionAddress)); - if (!exRecs.back().NumberParameters) - continue; - log << L"Parameters: "; - for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i) { - if (i != 0) - log << L", "; - log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]); + if (exRecs.back().NumberParameters) + { + log << L"Parameters: "; + for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i) + { + if (i != 0) + log << L", "; + log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]); + } } pRemoteExRec = exRecs.back().ExceptionRecord; - exRecs.emplace_back(); } - exRecs.pop_back(); } log << L"\nCall Stack\n{"; @@ -410,7 +418,7 @@ void print_exception_info_extended(const EXCEPTION_POINTERS& ex, const CONTEXT& std::wstring escape_shell_arg(const std::wstring& arg) { // https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way - + std::wstring res; if (!arg.empty() && arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) { res.append(arg); @@ -504,7 +512,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s filePath.emplace(pFilePath); std::fstream fileStream(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc); - + mz_zip_archive zipa{}; zipa.m_pIO_opaque = &fileStream; zipa.m_pRead = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t { @@ -566,7 +574,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s const auto hLogFile = CreateFileW(logFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); if (hLogFile == INVALID_HANDLE_VALUE) throw_last_error(std::format("indiv. log file: CreateFileW({})", ws_to_u8(logFilePath.wstring()))); - + std::unique_ptr hLogFileClose(hLogFile, &CloseHandle); LARGE_INTEGER size, baseOffset{}; @@ -695,7 +703,7 @@ int main() { // IFileSaveDialog only works on STA CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - + std::vector args; if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) { for (auto i = 0; i < argc; i++) @@ -823,14 +831,14 @@ int main() { hr = pOleWindow->GetWindow(&hwndProgressDialog); if (SUCCEEDED(hr)) { - SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0, + SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); SetForegroundWindow(hwndProgressDialog); } - + pOleWindow->Release(); } - + } else { std::cerr << "Failed to create progress window" << std::endl; @@ -852,14 +860,14 @@ int main() { https://github.com/sumatrapdfreader/sumatrapdf/blob/master/src/utils/DbgHelpDyn.cpp */ - + if (g_bSymbolsAvailable) { SymRefreshModuleList(g_hProcess); } else if(!assetDir.empty()) { auto symbol_search_path = std::format(L".;{}", (assetDir / "UIRes" / "pdb").wstring()); - + g_bSymbolsAvailable = SymInitializeW(g_hProcess, symbol_search_path.c_str(), true); std::wcout << std::format(L"Init symbols with PDB at {}", symbol_search_path) << std::endl; @@ -870,12 +878,12 @@ int main() { g_bSymbolsAvailable = SymInitializeW(g_hProcess, nullptr, true); std::cout << "Init symbols without PDB" << std::endl; } - + if (!g_bSymbolsAvailable) { std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl; } - if (pProgressDialog) + if (pProgressDialog) pProgressDialog->SetLine(3, L"Reading troubleshooting data", FALSE, NULL); std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0'); @@ -936,7 +944,7 @@ int main() { if (shutup) log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl; - + if (dumpPath.empty()) log << L"Dump skipped" << std::endl; else if (dumpError.empty()) @@ -1003,7 +1011,7 @@ int main() { R"aa(Help | Open log directory | Open log file)aa" ); #endif - + // Can't do this, xiv stops pumping messages here //config.hwndParent = FindWindowA("FFXIVGAME", NULL); @@ -1056,13 +1064,13 @@ int main() { return (*reinterpret_cast(dwRefData))(hwnd, uNotification, wParam, lParam); }; config.lpCallbackData = reinterpret_cast(&callback); - + if (pProgressDialog) { pProgressDialog->StopProgressDialog(); pProgressDialog->Release(); pProgressDialog = NULL; } - + const auto kill_game = [&] { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); }; if (shutup) { From 446c7e38771a943ac7b2dd9d5b48478f95d66250 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 6 Dec 2025 15:14:13 +0100 Subject: [PATCH 271/477] Some logging, cleanup --- Dalamud.Boot/dllmain.cpp | 26 +++++++++++++++----------- Dalamud.Boot/veh.cpp | 6 +++--- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 2af78092d..687089f82 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -336,19 +336,22 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { // This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it // was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now. // Ideally all of this will go away once they get to it. - static std::shared_ptr> s_hook; - s_hook = std::make_shared>("advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW"); - s_hook->set_detour([hook = s_hook.get()]( HANDLE hEventLog, - WORD wType, - WORD wCategory, - DWORD dwEventID, - PSID lpUserSid, - WORD wNumStrings, - DWORD dwDataSize, - LPCWSTR* lpStrings, - LPVOID lpRawData)->BOOL { + static std::shared_ptr> s_report_event_hook; + s_report_event_hook = std::make_shared>( + "advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW"); + s_report_event_hook->set_detour([hook = s_report_event_hook.get()]( + HANDLE hEventLog, + WORD wType, + WORD wCategory, + DWORD dwEventID, + PSID lpUserSid, + WORD wNumStrings, + DWORD dwDataSize, + LPCWSTR* lpStrings, + LPVOID lpRawData)-> BOOL { // Check for CLR Error Event IDs + // https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/eventreporter.cpp#L370 if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime @@ -371,6 +374,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { return original_ret; }); + logging::I("ReportEventW hook installed."); // ============================== Dalamud ==================================== // diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 24a846e66..25c9b5045 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -449,9 +449,9 @@ bool veh::remove_handler() return false; } -void veh::raise_external_event(const std::wstring& errorMessage) +void veh::raise_external_event(const std::wstring& info) { - const auto info_size = std::min(errorMessage.size(), std::size(g_external_event_info) - 1); - wcsncpy_s(g_external_event_info, errorMessage.c_str(), info_size); + const auto info_size = std::min(info.size(), std::size(g_external_event_info) - 1); + wcsncpy_s(g_external_event_info, info.c_str(), info_size); RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr); } From 1d1db04f04f98a353995f836c53a4e3918b683a6 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 6 Dec 2025 16:09:42 +0100 Subject: [PATCH 272/477] Use ImFontPtr in SeStringDrawState --- .../ImGuiSeStringRenderer/SeStringDrawState.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 11c1120b4..3a21e0db9 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -76,7 +76,7 @@ public unsafe ref struct SeStringDrawState this.splitter = default; this.GetEntity = ssdp.GetEntity; this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y)); - this.FontSizeScale = this.FontSize / this.Font->FontSize; + this.FontSizeScale = this.FontSize / this.Font.FontSize; this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight); this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f; this.Opacity = ssdp.EffectiveOpacity; @@ -106,7 +106,7 @@ public unsafe ref struct SeStringDrawState public Vector2 ScreenOffset { get; } /// - public ImFont* Font { get; } + public ImFontPtr Font { get; } /// public float FontSize { get; } @@ -256,7 +256,7 @@ public unsafe ref struct SeStringDrawState /// Offset of the glyph in pixels w.r.t. . internal void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset) { - var texId = this.Font->ContainerAtlas->Textures.Ref(g.TextureIndex).TexID; + var texId = this.Font.ContainerAtlas.Textures.Ref(g.TextureIndex).TexID; var xy0 = new Vector2( MathF.Round(g.X0 * this.FontSizeScale), MathF.Round(g.Y0 * this.FontSizeScale)); @@ -313,7 +313,7 @@ public unsafe ref struct SeStringDrawState offset += this.ScreenOffset; offset.Y += (this.LinkUnderlineThickness - 1) / 2f; - offset.Y += MathF.Round(((this.LineHeight - this.FontSize) / 2) + (this.Font->Ascent * this.FontSizeScale)); + offset.Y += MathF.Round(((this.LineHeight - this.FontSize) / 2) + (this.Font.Ascent * this.FontSizeScale)); this.SetCurrentChannel(SeStringDrawChannel.Foreground); this.DrawList.AddLine( @@ -340,9 +340,9 @@ public unsafe ref struct SeStringDrawState internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune) { var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue - ? this.Font->FindGlyph((ushort)rune.Value) - : this.Font->FallbackGlyph; - return ref *(ImGuiHelpers.ImFontGlyphReal*)p; + ? (ImFontGlyphPtr)this.Font.FindGlyph((ushort)rune.Value) + : this.Font.FallbackGlyph; + return ref *(ImGuiHelpers.ImFontGlyphReal*)p.Handle; } /// Gets the glyph corresponding to the given codepoint. @@ -375,7 +375,7 @@ public unsafe ref struct SeStringDrawState return 0; return MathF.Round( - this.Font->GetDistanceAdjustmentForPair( + this.Font.GetDistanceAdjustmentForPair( (ushort)left.Value, (ushort)right.Value) * this.FontSizeScale); } From e032840ac8e7ac13cd0493a30cac4398bcd86d94 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 6 Dec 2025 18:32:03 +0100 Subject: [PATCH 273/477] Clean up crash handler window log for external events --- Dalamud.Boot/crashhandler_shared.h | 2 ++ Dalamud.Boot/veh.cpp | 2 -- DalamudCrashHandler/DalamudCrashHandler.cpp | 26 ++++++++++++++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Dalamud.Boot/crashhandler_shared.h b/Dalamud.Boot/crashhandler_shared.h index 8d93e4460..0308306ce 100644 --- a/Dalamud.Boot/crashhandler_shared.h +++ b/Dalamud.Boot/crashhandler_shared.h @@ -6,6 +6,8 @@ #define WIN32_LEAN_AND_MEAN #include +#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679 + struct exception_info { LPEXCEPTION_POINTERS pExceptionPointers; diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 25c9b5045..50ac9b34c 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -11,8 +11,6 @@ #include "crashhandler_shared.h" #include "DalamudStartInfo.h" -#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679 - #pragma comment(lib, "comctl32.lib") #if defined _M_IX86 diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 107261541..1feec4b2f 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -938,9 +938,19 @@ int main() { } while (false); } + const bool is_external_event = exinfo.ExceptionRecord.ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT; + std::wostringstream log; - log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl; - log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl; + + if (!is_external_event) + { + log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl; + log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl; + } + else + { + log << L"CLR error occurred" << std::endl; + } if (shutup) log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl; @@ -957,9 +967,19 @@ int main() { if (pProgressDialog) pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL); + std::wstring window_log_str; + + // Cut the log here for external events, the rest is unreadable and doesn't matter since we can't get + // symbols for mixed-mode stacks yet. + if (is_external_event) + window_log_str = log.str(); + SymRefreshModuleList(GetCurrentProcess()); print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log); - const auto window_log_str = log.str(); + + if (!is_external_event) + window_log_str = log.str(); + print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log); std::wofstream(logPath) << log.str(); From b2d9480f9f83cb64e476d77f25068817beee6790 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 6 Dec 2025 18:38:13 +0100 Subject: [PATCH 274/477] Submit nuke schema --- .nuke/build.schema.json | 162 ++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 03211ce8f..6ffb3bb01 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,19 +1,57 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Build Schema", - "$ref": "#/definitions/build", "definitions": { - "build": { - "type": "object", + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "CI", + "Clean", + "Compile", + "CompileCImGui", + "CompileCImGuizmo", + "CompileCImPlot", + "CompileDalamud", + "CompileDalamudBoot", + "CompileDalamudCrashHandler", + "CompileImGuiNatives", + "CompileInjector", + "Restore", + "SetCILogging", + "Test" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { "properties": { - "Configuration": { - "type": "string", - "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", - "enum": [ - "Debug", - "Release" - ] - }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" @@ -23,29 +61,8 @@ "description": "Shows the help text for this build assembly" }, "Host": { - "type": "string", "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitbucket", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] - }, - "IsDocsBuild": { - "type": "boolean", - "description": "Whether we are building for documentation - emits generated files" + "$ref": "#/definitions/Host" }, "NoLogo": { "type": "boolean", @@ -74,63 +91,46 @@ "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { - "type": "string", - "enum": [ - "CI", - "Clean", - "Compile", - "CompileCImGui", - "CompileCImGuizmo", - "CompileCImPlot", - "CompileDalamud", - "CompileDalamudBoot", - "CompileDalamudCrashHandler", - "CompileImGuiNatives", - "CompileInjector", - "Restore", - "SetCILogging", - "Test" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "CI", - "Clean", - "Compile", - "CompileCImGui", - "CompileCImGuizmo", - "CompileCImPlot", - "CompileDalamud", - "CompileDalamudBoot", - "CompileDalamudCrashHandler", - "CompileImGuiNatives", - "CompileInjector", - "Restore", - "SetCILogging", - "Test" - ] + "$ref": "#/definitions/ExecutableTarget" } }, "Verbosity": { - "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] + "$ref": "#/definitions/Verbosity" } } } - } -} \ No newline at end of file + }, + "allOf": [ + { + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "IsDocsBuild": { + "type": "boolean", + "description": "Whether we are building for documentation - emits generated files" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + } + } + }, + { + "$ref": "#/definitions/NukeBuild" + } + ] +} From 3d29157391da8bc50a60c2aed640510647f257ba Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 6 Dec 2025 18:38:23 +0100 Subject: [PATCH 275/477] Revert "Add git status checks to workflow to see what's dirty" This reverts commit a36e11574b14ea6887cb0f7d2513920ebdd820bf. --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f552e446b..299d71e95 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,12 +33,8 @@ jobs: ($env:REPO_NAME) >> VERSION ($env:BRANCH) >> VERSION ($env:COMMIT) >> VERSION - - name: git status - run: git status - name: Build and Test Dalamud run: .\build.ps1 ci - - name: git status - run: git status - name: Sign Dalamud if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} env: From ab5ea34e686e7ead9673f0fb8492ca0a00aa5a09 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 6 Dec 2025 18:46:06 +0100 Subject: [PATCH 276/477] ci: make deploying builds globally blocking, don't cancel in-progress --- .github/workflows/main.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be44afacc..ea1afcac5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,10 @@ name: Build Dalamud on: [push, pull_request, workflow_dispatch] +# Globally blocking because of git pushes in deploy step concurrency: - group: build_dalamud_${{ github.ref_name }} - cancel-in-progress: true + group: build_dalamud_${{ github.repository_owner }} + cancel-in-progress: false jobs: build: From caa869d3ace57609680782b5862bb1d0820e86b5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 12:54:13 +0100 Subject: [PATCH 277/477] Clarify exception and docs regarding off-thread drawing with SeStrings, again --- .../Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs | 5 ++++- Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs index 1d8126f3b..972013328 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs @@ -14,8 +14,9 @@ public record struct SeStringDrawParams /// (the default). /// /// If this value is set, will not be called, and ImGui ID will be ignored. - /// You must specify a valid draw list and a valid font via if you set this value, + /// You must specify a valid draw list, a valid font via and if you set this value, /// since the renderer will not be able to retrieve them from ImGui context. + /// Must be set when drawing off the main thread. /// public ImDrawListPtr? TargetDrawList { get; set; } @@ -29,11 +30,13 @@ public record struct SeStringDrawParams /// Gets or sets the font to use. /// Font to use, or null to use (the default). + /// Must be set when specifying a target draw-list or drawing off the main thread. public ImFontPtr? Font { get; set; } /// Gets or sets the font size. /// Font size in pixels, or 0 to use the current ImGui font size . /// + /// Must be set when specifying a target draw-list or drawing off the main thread. public float? FontSize { get; set; } /// Gets or sets the line height ratio. diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 11c1120b4..04d0501b1 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -65,7 +65,7 @@ public unsafe ref struct SeStringDrawState this.drawList = ssdp.TargetDrawList.Value; this.ScreenOffset = Vector2.Zero; this.FontSize = ssdp.FontSize ?? throw new ArgumentException( - $"{nameof(ssdp.FontSize)} must be set to render outside the main thread."); + $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue; this.Color = ssdp.Color ?? uint.MaxValue; this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread. From b35faf13b53ce25e0f3f92977a75c4cb154a050b Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 13:04:11 +0100 Subject: [PATCH 278/477] Show unhandled exceptions through VEH --- Dalamud.Boot/veh.cpp | 6 +++++ Dalamud/EntryPoint.cs | 25 +++---------------- .../Interface/Internal/DalamudInterface.cs | 7 ++++++ Dalamud/Utility/ErrorHandling.cs | 21 ++++++++++++++++ DalamudCrashHandler/DalamudCrashHandler.cpp | 4 +-- 5 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 Dalamud/Utility/ErrorHandling.cs diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 50ac9b34c..194840e52 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -453,3 +453,9 @@ void veh::raise_external_event(const std::wstring& info) wcsncpy_s(g_external_event_info, info.c_str(), info_size); RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr); } + +extern "C" __declspec(dllexport) void BootVehRaiseExternalEventW(LPCWSTR info) +{ + const std::wstring info_wstr(info); + veh::raise_external_event(info_wstr); +} diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 15077f3d8..0f8cb0480 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -292,7 +292,6 @@ public sealed class EntryPoint } var pluginInfo = string.Empty; - var supportText = ", please visit us on Discord for more help"; try { var pm = Service.GetNullable(); @@ -300,9 +299,6 @@ public sealed class EntryPoint if (plugin != null) { pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n"; - - if (plugin.IsThirdParty) - supportText = string.Empty; } } catch @@ -310,31 +306,18 @@ public sealed class EntryPoint // ignored } - const MESSAGEBOX_STYLE flags = MESSAGEBOX_STYLE.MB_YESNO | MESSAGEBOX_STYLE.MB_ICONERROR | MESSAGEBOX_STYLE.MB_SYSTEMMODAL; - var result = Windows.Win32.PInvoke.MessageBox( - new HWND(Process.GetCurrentProcess().MainWindowHandle), - $"An internal error in a Dalamud plugin occurred.\nThe game must close.\n\n{ex.GetType().Name}\n{info}\n\n{pluginInfo}More information has been recorded separately{supportText}.\n\nDo you want to disable all plugins the next time you start the game?", - "Dalamud", - flags); - - if (result == MESSAGEBOX_RESULT.IDYES) - { - Log.Information("User chose to disable plugins on next launch..."); - var config = Service.Get(); - config.PluginSafeMode = true; - config.ForceSave(); - } - Log.CloseAndFlush(); - Environment.Exit(-1); + + ErrorHandling.CrashWithContext($"{ex}\n\n{info}\n\n{pluginInfo}"); break; default: Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject); Log.CloseAndFlush(); - Environment.Exit(-1); break; } + + Environment.Exit(-1); } private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index af78c5b0c..bf55a5486 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -667,6 +667,8 @@ internal class DalamudInterface : IInternalDisposableService { if (this.isImGuiDrawDevMenu) { + using var barColor = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0.060f, 0.060f, 0.060f, 0.773f)); + barColor.Push(ImGuiCol.MenuBarBg, Vector4.Zero); if (ImGui.BeginMainMenuBar()) { var pluginManager = Service.Get(); @@ -839,6 +841,11 @@ internal class DalamudInterface : IInternalDisposableService ImGui.PopStyleVar(); } + if (ImGui.MenuItem("Raise external event through boot")) + { + ErrorHandling.CrashWithContext("Tést"); + } + ImGui.EndMenu(); } diff --git a/Dalamud/Utility/ErrorHandling.cs b/Dalamud/Utility/ErrorHandling.cs new file mode 100644 index 000000000..3c025a12e --- /dev/null +++ b/Dalamud/Utility/ErrorHandling.cs @@ -0,0 +1,21 @@ +using System.Runtime.InteropServices; + +namespace Dalamud.Utility; + +/// +/// Utilities for handling errors inside Dalamud. +/// +internal static partial class ErrorHandling +{ + /// + /// Crash the game at this point, and show the crash handler with the supplied context. + /// + /// The context to show in the crash handler. + public static void CrashWithContext(string context) + { + BootVehRaiseExternalEvent(context); + } + + [LibraryImport("Dalamud.Boot.dll", EntryPoint = "BootVehRaiseExternalEventW", StringMarshalling = StringMarshalling.Utf16)] + private static partial void BootVehRaiseExternalEvent(string info); +} diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 1feec4b2f..3955bd983 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -1014,8 +1014,8 @@ int main() { config.pButtons = buttons; config.cButtons = ARRAYSIZE(buttons); config.nDefaultButton = IdButtonRestart; - config.pszExpandedControlText = L"Hide stack trace"; - config.pszCollapsedControlText = L"Stack trace for plugin developers"; + config.pszExpandedControlText = L"Hide further information"; + config.pszCollapsedControlText = L"Further information for developers"; config.pszExpandedInformation = window_log_str.c_str(); config.pszWindowTitle = L"Dalamud Crash Handler"; config.pRadioButtons = radios; From 9cfa81c92d54cf1d8f8d8c08916ecabf316358eb Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 14:57:28 +0100 Subject: [PATCH 279/477] Remove unused packages --- Dalamud/Dalamud.csproj | 3 --- Directory.Packages.props | 3 --- 2 files changed, 6 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1c16891b7..a13df8cae 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -84,11 +84,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/Directory.Packages.props b/Directory.Packages.props index 58e355400..a94aae7c5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,11 +17,8 @@ - - - From 07f9e03010c53e2bda0422b6fada960072ff1657 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 15:13:35 +0100 Subject: [PATCH 280/477] Update packages --- Directory.Packages.props | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a94aae7c5..ec2e7e276 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,35 +6,35 @@ - + - + - + - + - + - + - - - - + + + + - - + + @@ -49,15 +49,15 @@ - - + + - - - - - - - + + + + + + + From 9e5723359a2ab36915488edb5612776e7da93c00 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 15:35:38 +0100 Subject: [PATCH 281/477] Remove obsolete casts from Lumina.Text.SeString --- Dalamud/Game/Text/Evaluator/SeStringParameter.cs | 4 ---- Dalamud/Game/Text/SeStringHandling/SeString.cs | 8 -------- 2 files changed, 12 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs index 1c6dd96cb..036d1c921 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs @@ -3,7 +3,6 @@ using System.Globalization; using Lumina.Text.ReadOnly; using DSeString = Dalamud.Game.Text.SeStringHandling.SeString; -using LSeString = Lumina.Text.SeString; namespace Dalamud.Game.Text.Evaluator; @@ -71,9 +70,6 @@ public readonly struct SeStringParameter public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value)); - [Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)] - public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData)); - public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode())); public static implicit operator SeStringParameter(string value) => new(value); diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 8805c2177..a1ef5e936 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -113,14 +113,6 @@ public class SeString /// Equivalent SeString. public static implicit operator SeString(string str) => new(new TextPayload(str)); - /// - /// Implicitly convert a string into a SeString containing a . - /// - /// string to convert. - /// Equivalent SeString. - [Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)] - public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString(); - /// /// Parse a binary game message into an SeString. /// From d4fe523d73925944d5151d7d6ee7428034ef4cf0 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 15:38:10 +0100 Subject: [PATCH 282/477] Clean up some warnings --- Dalamud/Interface/Windowing/Window.cs | 2 +- Dalamud/Utility/Util.cs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index b0786fbb5..48352daa2 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -908,7 +908,7 @@ public abstract class Window private void DrawErrorMessage() { // TODO: Once window systems are services, offer to reload the plugin - ImGui.TextColoredWrapped(ImGuiColors.DalamudRed,Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); ImGuiHelpers.ScaledDummy(5); diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index f50efcf0d..bde113904 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -158,16 +158,6 @@ public static partial class Util return branchInternal = gitBranch; } - /// - /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version - /// downloaded from webservices. - /// - /// The name of the track, or null. - internal static string? GetActiveTrack() - { - return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); - } - /// public static unsafe string DescribeAddress(void* p) => DescribeAddress((nint)p); @@ -703,6 +693,16 @@ public static partial class Util } } + /// + /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version + /// downloaded from webservices. + /// + /// The name of the track, or null. + internal static string? GetActiveTrack() + { + return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); + } + /// /// Gets a random, inoffensive, human-friendly string. /// From c50237cf6604f32de59c070667abab6813534b84 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 15:46:01 +0100 Subject: [PATCH 283/477] Add compatibility changes for SeString API breakage --- .../Internal/SeStringRenderer.cs | 4 +++- .../ImGuiSeStringRenderer/SeStringDrawState.cs | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index 87df2da2c..397502b30 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -161,7 +161,9 @@ internal class SeStringRenderer : IServiceType ImFont* font = null; if (drawParams.Font.HasValue) font = drawParams.Font.Value; - if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null) + + // API14: Remove commented out code + if (ThreadSafety.IsMainThread /* && drawParams.TargetDrawList is null */ && font is null) font = ImGui.GetFont(); if (font is null) throw new ArgumentException("Specified font is empty."); diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 04d0501b1..5e63ef160 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -64,8 +64,20 @@ public unsafe ref struct SeStringDrawState { this.drawList = ssdp.TargetDrawList.Value; this.ScreenOffset = Vector2.Zero; - this.FontSize = ssdp.FontSize ?? throw new ArgumentException( - $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); + + // API14: Remove, always throw + if (ThreadSafety.IsMainThread) + { + this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); + } + else + { + throw new ArgumentException( + $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); + } + + // this.FontSize = ssdp.FontSize ?? throw new ArgumentException( + // $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); this.WrapWidth = ssdp.WrapWidth ?? float.MaxValue; this.Color = ssdp.Color ?? uint.MaxValue; this.LinkHoverBackColor = 0; // Interactivity is unused outside the main thread. From 094483e5a08315b218f1e20277db44df360cbc2f Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 15:52:13 +0100 Subject: [PATCH 284/477] List PRs in changelog generator --- .github/generate_changelog.py | 209 +++++++++++++++++------ .github/workflows/generate-changelog.yml | 24 +-- 2 files changed, 166 insertions(+), 67 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 5e921fd6e..b07100115 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -8,7 +8,8 @@ import re import sys import json import argparse -from typing import List, Tuple, Optional +import os +from typing import List, Tuple, Optional, Dict, Any def run_git_command(args: List[str]) -> str: @@ -30,14 +31,14 @@ def get_last_two_tags() -> Tuple[str, str]: """Get the latest two git tags.""" tags = run_git_command(["tag", "--sort=-version:refname"]) tag_list = [t for t in tags.split("\n") if t] - + # Filter out old tags that start with 'v' (old versioning scheme) tag_list = [t for t in tag_list if not t.startswith('v')] if len(tag_list) < 2: print("Error: Need at least 2 tags in the repository", file=sys.stderr) sys.exit(1) - + return tag_list[0], tag_list[1] @@ -55,58 +56,144 @@ def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]: return None -def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]: - """Get commits between two tags. Returns list of (message, author) tuples.""" +def get_repo_info() -> Tuple[str, str]: + """Get repository owner and name from git remote.""" + try: + remote_url = run_git_command(["config", "--get", "remote.origin.url"]) + + # Handle both HTTPS and SSH URLs + # SSH: git@github.com:owner/repo.git + # HTTPS: https://github.com/owner/repo.git + match = re.search(r'github\.com[:/](.+?)/(.+?)(?:\.git)?$', remote_url) + if match: + owner = match.group(1) + repo = match.group(2) + return owner, repo + else: + print("Error: Could not parse GitHub repository from remote URL", file=sys.stderr) + sys.exit(1) + except: + print("Error: Could not get git remote URL", file=sys.stderr) + sys.exit(1) + + +def get_commits_between_tags(tag1: str, tag2: str) -> List[str]: + """Get commit SHAs between two tags.""" log_output = run_git_command([ "log", f"{tag2}..{tag1}", - "--format=%s|%an|%h" + "--format=%H" ]) - - commits = [] - for line in log_output.split("\n"): - if "|" in line: - message, author, sha = line.split("|", 2) - commits.append((message.strip(), author.strip(), sha.strip())) - + + commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()] return commits -def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]: - """Filter out commits matching any of the ignore patterns.""" +def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]: + """Get PR information for a commit using GitHub API.""" + try: + import requests + except ImportError: + print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr) + sys.exit(1) + + headers = { + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28" + } + + if token: + headers["Authorization"] = f"Bearer {token}" + + url = f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}/pulls" + + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + prs = response.json() + + if prs and len(prs) > 0: + # Return the first PR (most relevant one) + pr = prs[0] + return { + "number": pr["number"], + "title": pr["title"], + "author": pr["user"]["login"], + "url": pr["html_url"] + } + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + # Commit might not be associated with a PR + return None + elif e.response.status_code == 403: + print("Warning: GitHub API rate limit exceeded. Consider providing a token.", file=sys.stderr) + return None + else: + print(f"Warning: Failed to fetch PR for commit {commit_sha[:7]}: {e}", file=sys.stderr) + return None + except Exception as e: + print(f"Warning: Error fetching PR for commit {commit_sha[:7]}: {e}", file=sys.stderr) + return None + + return None + + +def get_prs_between_tags(tag1: str, tag2: str, owner: str, repo: str, token: str) -> List[Dict[str, Any]]: + """Get PRs between two tags using GitHub API.""" + commits = get_commits_between_tags(tag1, tag2) + print(f"Found {len(commits)} commits, fetching PR information...") + + prs = [] + seen_pr_numbers = set() + + for i, commit_sha in enumerate(commits, 1): + if i % 10 == 0: + print(f"Progress: {i}/{len(commits)} commits processed...") + + pr_info = get_pr_for_commit(commit_sha, owner, repo, token) + if pr_info and pr_info["number"] not in seen_pr_numbers: + seen_pr_numbers.add(pr_info["number"]) + prs.append(pr_info) + + return prs + + +def filter_prs(prs: List[Dict[str, Any]], ignore_patterns: List[str]) -> List[Dict[str, Any]]: + """Filter out PRs matching any of the ignore patterns.""" compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns] - + filtered = [] - for message, author, sha in commits: - if not any(pattern.search(message) for pattern in compiled_patterns): - filtered.append((message, author, sha)) - + for pr in prs: + if not any(pattern.search(pr["title"]) for pattern in compiled_patterns): + filtered.append(pr) + return filtered -def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]], - cs_commit_new: Optional[str], cs_commit_old: Optional[str]) -> str: +def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]], + cs_commit_new: Optional[str], cs_commit_old: Optional[str], + owner: str, repo: str) -> str: """Generate markdown changelog.""" # Calculate statistics - commit_count = len(commits) - unique_authors = len(set(author for _, author, _ in commits)) - + pr_count = len(prs) + unique_authors = len(set(pr["author"] for pr in prs)) + changelog = f"# Dalamud Release v{version}\n\n" changelog += f"We just released Dalamud v{version}, which should be available to users within a few minutes. " - changelog += f"This release includes **{commit_count} commit{'s' if commit_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n" - changelog += f"[Click here]() to see all Dalamud changes.\n\n" - + changelog += f"This release includes **{pr_count} PR{'s' if pr_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n" + changelog += f"[Click here]() to see all Dalamud changes.\n\n" + if cs_commit_new and cs_commit_old and cs_commit_new != cs_commit_old: changelog += f"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`]()**.\n" changelog += f"[Click here]() to see all CS changes.\n" elif cs_commit_new: changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`]()**.\n" - + changelog += "## Dalamud Changes\n\n" - - for message, author, sha in commits: - changelog += f"* {message} (by **{author}** as [`{sha}`]())\n" - + + for pr in prs: + changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n" + return changelog @@ -117,9 +204,9 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None: except ImportError: print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr) sys.exit(1) - + filename = f"changelog-v{version}.md" - + # Prepare the payload data = { "content": f"Dalamud v{version} has been released!", @@ -130,13 +217,13 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None: } ] } - + # Prepare the files files = { "payload_json": (None, json.dumps(data)), "files[0]": (filename, content.encode('utf-8'), 'text/markdown') } - + try: result = requests.post(webhook_url, files=files) result.raise_for_status() @@ -158,54 +245,64 @@ def main(): required=True, help="Discord webhook URL" ) + parser.add_argument( + "--github-token", + default=os.environ.get("GITHUB_TOKEN"), + help="GitHub API token (or set GITHUB_TOKEN env var). Increases rate limit." + ) parser.add_argument( "--ignore", action="append", default=[], - help="Regex patterns to ignore commits (can be specified multiple times)" + help="Regex patterns to ignore PRs (can be specified multiple times)" ) parser.add_argument( "--submodule-path", default="lib/FFXIVClientStructs", help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)" ) - + args = parser.parse_args() - + + # Get repository info + owner, repo = get_repo_info() + print(f"Repository: {owner}/{repo}") + # Get the last two tags latest_tag, previous_tag = get_last_two_tags() print(f"Generating changelog between {previous_tag} and {latest_tag}") - + # Get submodule commits at both tags cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag) cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag) - + if cs_commit_new: print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}") if cs_commit_old: print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}") - - # Get commits between tags - commits = get_commits_between_tags(latest_tag, previous_tag) - print(f"Found {len(commits)} commits") - - # Filter commits - filtered_commits = filter_commits(commits, args.ignore) - print(f"After filtering: {len(filtered_commits)} commits") - + + # Get PRs between tags + prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token) + prs.reverse() + print(f"Found {len(prs)} PRs") + + # Filter PRs + filtered_prs = filter_prs(prs, args.ignore) + print(f"After filtering: {len(filtered_prs)} PRs") + # Generate changelog - changelog = generate_changelog(latest_tag, previous_tag, filtered_commits, - cs_commit_new, cs_commit_old) - + changelog = generate_changelog(latest_tag, previous_tag, filtered_prs, + cs_commit_new, cs_commit_old, owner, repo) + print("\n" + "="*50) print("Generated Changelog:") print("="*50) print(changelog) print("="*50 + "\n") - + # Post to Discord post_to_discord(args.webhook_url, changelog, latest_tag) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml index 5fed3b1eb..e62d5f37c 100644 --- a/.github/workflows/generate-changelog.yml +++ b/.github/workflows/generate-changelog.yml @@ -6,41 +6,43 @@ on: tags: - '*' +permissions: read-all + jobs: generate-changelog: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch all history and tags submodules: true # Fetch submodules - + - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.14' - + - name: Install dependencies run: | python -m pip install --upgrade pip pip install requests - + - name: Generate and post changelog + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_TERMINAL_PROMPT: 0 run: | python .github/generate_changelog.py \ --webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \ - --ignore "^Merge" \ - --ignore "^build:" \ - --ignore "^docs:" - env: - GIT_TERMINAL_PROMPT: 0 - + --ignore "Update ClientStructs" \ + --ignore "^build:" + - name: Upload changelog as artifact if: always() uses: actions/upload-artifact@v4 with: name: changelog path: changelog-*.md - if-no-files-found: ignore \ No newline at end of file + if-no-files-found: ignore From 652ff59672efa8abf1a7cba81898431be4b61c81 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 15:52:26 +0100 Subject: [PATCH 285/477] build: 13.0.0.13 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index d1f730d5e..a50f12d79 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.12 + 13.0.0.13 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 8a5f1fd96d7192c76602b90bdbad1e95d8c03f26 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 15:55:43 +0100 Subject: [PATCH 286/477] Add PluginUISoundEffectsEnabled to UiBuilder --- Dalamud/Interface/UiBuilder.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 9f60aba6a..ea0e21e97 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -219,6 +219,12 @@ public interface IUiBuilder /// bool ShouldUseReducedMotion { get; } + /// + /// Gets a value indicating whether the user has enabled the "Enable sound effects for plugin windows" setting.
+ /// This setting is effected by the in-game "System Sounds" option and volume. + ///
+ bool PluginUISoundEffectsEnabled { get; } + /// /// Loads an ULD file that can load textures containing multiple icons in a single texture. /// @@ -560,6 +566,9 @@ public sealed class UiBuilder : IDisposable, IUiBuilder ///
public bool ShouldUseReducedMotion => Service.Get().ReduceMotions ?? false; + /// + public bool PluginUISoundEffectsEnabled => Service.Get().EnablePluginUISoundEffects; + /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// From 7199bfb0a90f886cd0c831c0eeed6303fbde4fcb Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 16:20:55 +0100 Subject: [PATCH 287/477] Remove targets --- targets/Dalamud.Plugin.Bootstrap.targets | 11 -------- targets/Dalamud.Plugin.targets | 35 ------------------------ 2 files changed, 46 deletions(-) delete mode 100644 targets/Dalamud.Plugin.Bootstrap.targets delete mode 100644 targets/Dalamud.Plugin.targets diff --git a/targets/Dalamud.Plugin.Bootstrap.targets b/targets/Dalamud.Plugin.Bootstrap.targets deleted file mode 100644 index db4bf6cd7..000000000 --- a/targets/Dalamud.Plugin.Bootstrap.targets +++ /dev/null @@ -1,11 +0,0 @@ - - - - $(appdata)\XIVLauncher\addon\Hooks\dev\ - $(HOME)/.xlcore/dalamud/Hooks/dev/ - $(HOME)/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/ - $(DALAMUD_HOME)/ - - - - diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets deleted file mode 100644 index 08d19735e..000000000 --- a/targets/Dalamud.Plugin.targets +++ /dev/null @@ -1,35 +0,0 @@ - - - - net8.0-windows - x64 - enable - latest - true - false - false - true - true - $(AssemblySearchPaths);$(DalamudLibPath) - - - - - - - - - - - - - - - - - - - - - - From 2f5f52b572850342ef09ec9fab81f6d564e651fe Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 16:23:13 +0100 Subject: [PATCH 288/477] Forgot to remove this too --- Dalamud/Dalamud.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1c16891b7..d1b77b1fc 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -227,9 +227,4 @@ - - - - - From c254c8600e19f134ed4d526a7886e2620677f14f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 16:31:03 +0100 Subject: [PATCH 289/477] Update Font Awesome to 7.1.0 --- .../Interface/FontAwesome/FontAwesomeIcon.cs | 1171 ++++++++++------- 1 file changed, 671 insertions(+), 500 deletions(-) diff --git a/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs b/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs index f88d7f8f0..35df9cfbc 100644 --- a/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs +++ b/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // Generated by Dalamud.FASharpGen - don't modify this file directly. -// Font-Awesome Version: 6.4.2 +// Font-Awesome Version: 7.1.0 // //------------------------------------------------------------------------------ @@ -29,14 +29,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "address-book" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "address book", "contact", "directory", "index", "little black book", "rolodex" })] + [FontAwesomeSearchTerms(new[] { "address book", "contact", "directory", "employee", "index", "little black book", "portfolio", "rolodex", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Users + People" })] AddressBook = 0xF2B9, /// /// The Font Awesome "address-card" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "address card", "about", "contact", "id", "identification", "postcard", "profile", "registration" })] + [FontAwesomeSearchTerms(new[] { "address card", "about", "contact", "employee", "id", "identification", "portfolio", "postcard", "profile", "registration", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Alphabet", "Business", "Communication", "Users + People" })] AddressCard = 0xF2BB, @@ -54,6 +54,13 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Automotive" })] AirFreshener = 0xF5D0, + /// + /// The Font Awesome "alarm-clock" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "alarm clock", "alarm", "alarm clock", "clock", "date", "late", "pending", "reminder", "sleep", "snooze", "timer", "timestamp", "watch" })] + [FontAwesomeCategoriesAttribute(new[] { "Alert", "Time", "Travel + Hotel" })] + AlarmClock = 0xF34E, + /// /// The Font Awesome "align-center" icon unicode character. /// @@ -113,28 +120,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "anchor-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anchor circle check", "marina", "not affected", "ok", "okay", "port" })] + [FontAwesomeSearchTerms(new[] { "anchor circle check", "enable", "marina", "not affected", "ok", "okay", "port", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime" })] AnchorCircleCheck = 0xE4AA, /// /// The Font Awesome "anchor-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anchor circle exclamation", "affected", "marina", "port" })] + [FontAwesomeSearchTerms(new[] { "anchor circle exclamation", "affected", "failed", "marina", "port" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime" })] AnchorCircleExclamation = 0xE4AB, /// /// The Font Awesome "anchor-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anchor circle xmark", "destroy", "marina", "port" })] + [FontAwesomeSearchTerms(new[] { "anchor circle xmark", "destroy", "marina", "port", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime" })] AnchorCircleXmark = 0xE4AC, /// /// The Font Awesome "anchor-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anchor lock", "closed", "lockdown", "marina", "port", "quarantine" })] + [FontAwesomeSearchTerms(new[] { "anchor lock", "closed", "lockdown", "marina", "padlock", "port", "privacy", "quarantine" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime" })] AnchorLock = 0xE4AD, @@ -169,7 +176,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "angle-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "angle down", "down arrowhead", "arrow", "caret", "download", "expand" })] + [FontAwesomeSearchTerms(new[] { "angle down", "down arrowhead", "arrow", "caret", "download", "expand", "insert" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] AngleDown = 0xF107, @@ -190,7 +197,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "angle-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "angle up", "up arrowhead", "arrow", "caret", "collapse", "upload" })] + [FontAwesomeSearchTerms(new[] { "angle up", "up arrowhead", "arrow", "caret", "collapse", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] AngleUp = 0xF106, @@ -253,7 +260,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "circle-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle up", "arrow-circle-o-up" })] + [FontAwesomeSearchTerms(new[] { "circle up", "arrow-circle-o-up", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowAltCircleUp = 0xF35B, @@ -281,7 +288,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "circle-arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle arrow up", "upload" })] + [FontAwesomeSearchTerms(new[] { "circle arrow up", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowCircleUp = 0xF0AA, @@ -309,7 +316,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-down-up-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow down up lock", "border", "closed", "crossing", "lockdown", "quarantine", "transfer" })] + [FontAwesomeSearchTerms(new[] { "arrow down up lock", "border", "closed", "crossing", "lockdown", "padlock", "privacy", "quarantine", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowDownUpLock = 0xE4B0, @@ -337,7 +344,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-right-arrow-left" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow right arrow left", "rightwards arrow over leftwards arrow", "arrow", "arrows", "reciprocate", "return", "swap", "transfer" })] + [FontAwesomeSearchTerms(new[] { "arrow right arrow left", "arrow", "arrows", "reciprocate", "return", "swap", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowRightArrowLeft = 0xF0EC, @@ -358,14 +365,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-right-to-bracket" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow right to bracket", "arrow", "enter", "join", "log in", "login", "sign in", "sign up", "sign-in", "signin", "signup" })] + [FontAwesomeSearchTerms(new[] { "arrow right to bracket", "arrow", "enter", "insert", "join", "log in", "login", "sign in", "sign up", "sign-in", "signin", "signup" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowRightToBracket = 0xF090, /// /// The Font Awesome "arrow-right-to-city" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow right to city", "building", "city", "exodus", "rural", "urban" })] + [FontAwesomeSearchTerms(new[] { "arrow right to city", "building", "city", "exodus", "insert", "rural", "urban" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] ArrowRightToCity = 0xE4B3, @@ -393,14 +400,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-down-to-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows down to line", "scale down", "sink" })] + [FontAwesomeSearchTerms(new[] { "arrows down to line", "insert", "scale down", "sink" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowsDownToLine = 0xE4B8, /// /// The Font Awesome "arrows-down-to-people" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows down to people", "affected", "focus", "targeted" })] + [FontAwesomeSearchTerms(new[] { "arrows down to people", "affected", "focus", "insert", "targeted", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] ArrowsDownToPeople = 0xE4B9, @@ -435,14 +442,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-to-circle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows to circle", "center", "concentrate", "coordinate", "coordination", "focal point", "focus" })] + [FontAwesomeSearchTerms(new[] { "arrows to circle", "center", "concentrate", "coordinate", "coordination", "focal point", "focus", "insert" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowsToCircle = 0xE4BD, /// /// The Font Awesome "arrows-to-dot" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows to dot", "assembly point", "center", "condense", "focus", "minimize" })] + [FontAwesomeSearchTerms(new[] { "arrows to dot", "assembly point", "center", "condense", "focus", "insert", "minimize" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Business", "Humanitarian", "Marketing" })] ArrowsToDot = 0xE4BE, @@ -463,7 +470,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-turn-to-dots" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows turn to dots", "destination", "nexus" })] + [FontAwesomeSearchTerms(new[] { "arrows turn to dots", "destination", "insert", "nexus" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowsTurnToDots = 0xE4C1, @@ -484,7 +491,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-up-to-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows up to line", "rise", "scale up" })] + [FontAwesomeSearchTerms(new[] { "arrows up to line", "rise", "scale up", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowsUpToLine = 0xE4C2, @@ -519,28 +526,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up", "upwards arrow", "forward", "upload" })] + [FontAwesomeSearchTerms(new[] { "arrow up", "upwards arrow", "forward", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowUp = 0xF062, /// /// The Font Awesome "arrow-up-from-bracket" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up from bracket", "share", "transfer", "upload" })] + [FontAwesomeSearchTerms(new[] { "arrow up from bracket", "share", "transfer", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowUpFromBracket = 0xE09A, /// /// The Font Awesome "arrow-up-from-ground-water" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up from ground water", "groundwater", "spring", "water supply", "water table" })] + [FontAwesomeSearchTerms(new[] { "arrow up from ground water", "groundwater", "spring", "upgrade", "water supply", "water table" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Energy", "Humanitarian" })] ArrowUpFromGroundWater = 0xE4B5, /// /// The Font Awesome "arrow-up-from-water-pump" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up from water pump", "flood", "groundwater", "pump", "submersible", "sump pump" })] + [FontAwesomeSearchTerms(new[] { "arrow up from water pump", "flood", "groundwater", "pump", "submersible", "sump pump", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Humanitarian" })] ArrowUpFromWaterPump = 0xE4B6, @@ -554,14 +561,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-up-right-dots" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up right dots", "growth", "increase", "population" })] + [FontAwesomeSearchTerms(new[] { "arrow up right dots", "growth", "increase", "population", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowUpRightDots = 0xE4B7, /// /// The Font Awesome "arrow-up-right-from-square" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up right from square", "new", "open", "send", "share" })] + [FontAwesomeSearchTerms(new[] { "arrow up right from square", "new", "open", "send", "share", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowUpRightFromSquare = 0xF08E, @@ -576,7 +583,7 @@ public enum FontAwesomeIcon /// The Font Awesome "asterisk" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x2A. ///
- [FontAwesomeSearchTerms(new[] { "asterisk", "asterisk", "heavy asterisk", "annotation", "details", "reference", "star" })] + [FontAwesomeSearchTerms(new[] { "asterisk", "asterisk", "heavy asterisk", "annotation", "details", "reference", "required", "star" })] [FontAwesomeCategoriesAttribute(new[] { "Punctuation + Symbols", "Spinners" })] Asterisk = 0xF069, @@ -591,14 +598,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "book-atlas" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "book atlas", "book", "directions", "geography", "globe", "library", "map", "research", "travel", "wayfinding" })] + [FontAwesomeSearchTerms(new[] { "book atlas", "book", "directions", "geography", "globe", "knowledge", "library", "map", "research", "travel", "wayfinding" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Travel + Hotel" })] Atlas = 0xF558, /// /// The Font Awesome "atom" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "atheism", "atheist", "atom", "atom symbol", "chemistry", "electron", "ion", "isotope", "neutron", "nuclear", "proton", "science" })] + [FontAwesomeSearchTerms(new[] { "atheism", "atheist", "atom", "atom symbol", "chemistry", "electron", "ion", "isotope", "knowledge", "neutron", "nuclear", "proton", "science" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Energy", "Religion", "Science", "Science Fiction", "Spinners" })] Atom = 0xF5D2, @@ -619,14 +626,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "award" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "award", "honor", "praise", "prize", "recognition", "ribbon", "trophy" })] + [FontAwesomeSearchTerms(new[] { "award", "guarantee", "honor", "praise", "prize", "recognition", "ribbon", "trophy", "warranty" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Political" })] Award = 0xF559, /// /// The Font Awesome "baby" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "baby", "users-people" })] + [FontAwesomeSearchTerms(new[] { "baby", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Humanitarian", "Users + People" })] Baby = 0xF77C, @@ -668,7 +675,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bacterium" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bacterium", "antibiotic", "antibody", "covid-19", "health", "organism", "sick" })] + [FontAwesomeSearchTerms(new[] { "bacterium", "antibiotic", "antibody", "covid-19", "germ", "health", "organism", "sick" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] Bacterium = 0xE05A, @@ -710,14 +717,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "ban" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "abort", "ban", "block", "cancel", "delete", "entry", "forbidden", "hide", "no", "not", "prohibit", "prohibited", "remove", "stop", "trash" })] + [FontAwesomeSearchTerms(new[] { "404", "abort", "ban", "block", "cancel", "circle", "delete", "deny", "disabled", "entry", "failed", "forbidden", "hide", "no", "not", "not found", "prohibit", "prohibited", "remove", "slash", "stop", "trash" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security" })] Ban = 0xF05E, /// /// The Font Awesome "bandage" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "adhesive bandage", "bandage", "boo boo", "first aid", "ouch" })] + [FontAwesomeSearchTerms(new[] { "adhesive bandage", "bandage", "boo boo", "first aid", "modify", "ouch" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Medical + Health" })] BandAid = 0xF462, @@ -815,7 +822,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bed" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bed", "hospital", "hotel", "lodging", "mattress", "patient", "person in bed", "rest", "sleep", "travel" })] + [FontAwesomeSearchTerms(new[] { "bed", "hospital", "hotel", "lodging", "mattress", "patient", "person in bed", "rest", "sleep", "travel", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Humanitarian", "Maps", "Travel + Hotel", "Users + People" })] Bed = 0xF236, @@ -829,7 +836,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bell" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "alarm", "alert", "bel", "bell", "chime", "notification", "reminder" })] + [FontAwesomeSearchTerms(new[] { "alarm", "alert", "bel", "bell", "chime", "notification", "reminder", "request" })] [FontAwesomeCategoriesAttribute(new[] { "Alert", "Education", "Household", "Maps", "Shopping", "Social", "Time" })] Bell = 0xF0F3, @@ -864,14 +871,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-biking" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person biking", "bicycle", "bike", "biking", "cyclist", "pedal", "person biking", "summer", "wheel" })] + [FontAwesomeSearchTerms(new[] { "person biking", "bicycle", "bike", "biking", "cyclist", "pedal", "person biking", "summer", "uer", "wheel" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Sports + Fitness", "Users + People" })] Biking = 0xF84A, /// /// The Font Awesome "binoculars" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "binoculars", "glasses", "magnify", "scenic", "spyglass", "view" })] + [FontAwesomeSearchTerms(new[] { "binoculars", "glasses", "inspection", "magnifier", "magnify", "scenic", "spyglass", "view" })] [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Camping", "Maps", "Nature" })] Binoculars = 0xF1E5, @@ -913,7 +920,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-walking-with-cane" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking with cane", "blind", "cane" })] + [FontAwesomeSearchTerms(new[] { "person walking with cane", "blind", "cane", "follow", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Maps", "Users + People" })] Blind = 0xF29D, @@ -969,14 +976,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "book" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "book", "cover", "decorated", "diary", "documentation", "journal", "library", "notebook", "notebook with decorative cover", "read", "research" })] + [FontAwesomeSearchTerms(new[] { "book", "cover", "decorated", "diary", "documentation", "journal", "knowledge", "library", "notebook", "notebook with decorative cover", "read", "research", "scholar" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Maps", "Writing" })] Book = 0xF02D, /// /// The Font Awesome "book-bookmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "book bookmark", "library", "research" })] + [FontAwesomeSearchTerms(new[] { "book bookmark", "knowledge", "library", "research" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Writing" })] BookBookmark = 0xE0BB, @@ -1004,7 +1011,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "book-open" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "book open", "book", "book", "flyer", "library", "notebook", "open", "open book", "pamphlet", "reading", "research" })] + [FontAwesomeSearchTerms(new[] { "book open", "book", "book", "flyer", "knowledge", "library", "notebook", "open", "open book", "pamphlet", "reading", "research" })] [FontAwesomeCategoriesAttribute(new[] { "Education" })] BookOpen = 0xF518, @@ -1130,7 +1137,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "brain" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "brain", "cerebellum", "gray matter", "intellect", "intelligent", "medulla oblongata", "mind", "noodle", "wit" })] + [FontAwesomeSearchTerms(new[] { "brain", "cerebellum", "gray matter", "intellect", "intelligent", "knowledge", "medulla oblongata", "mind", "noodle", "scholar", "wit" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Science" })] Brain = 0xF5DC, @@ -1158,28 +1165,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bridge-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bridge circle check", "bridge", "not affected", "ok", "okay", "road" })] + [FontAwesomeSearchTerms(new[] { "bridge circle check", "bridge", "enable", "not affected", "ok", "okay", "road", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] BridgeCircleCheck = 0xE4C9, /// /// The Font Awesome "bridge-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bridge circle exclamation", "affected", "bridge", "road" })] + [FontAwesomeSearchTerms(new[] { "bridge circle exclamation", "affected", "bridge", "failed", "road" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] BridgeCircleExclamation = 0xE4CA, /// /// The Font Awesome "bridge-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bridge circle xmark", "bridge", "destroy", "road" })] + [FontAwesomeSearchTerms(new[] { "bridge circle xmark", "bridge", "destroy", "road", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] BridgeCircleXmark = 0xE4CB, /// /// The Font Awesome "bridge-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bridge lock", "bridge", "closed", "lockdown", "quarantine", "road" })] + [FontAwesomeSearchTerms(new[] { "bridge lock", "bridge", "closed", "lockdown", "padlock", "privacy", "quarantine", "road" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] BridgeLock = 0xE4CC, @@ -1193,7 +1200,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "briefcase" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bag", "briefcas", "briefcase", "business", "luggage", "office", "work" })] + [FontAwesomeSearchTerms(new[] { "bag", "briefcas", "briefcase", "business", "luggage", "offer", "office", "portfolio", "work" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Maps", "Travel + Hotel" })] Briefcase = 0xF0B1, @@ -1207,7 +1214,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "tower-broadcast" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tower broadcast", "airwaves", "antenna", "communication", "emergency", "radio", "reception", "waves" })] + [FontAwesomeSearchTerms(new[] { "tower broadcast", "airwaves", "antenna", "communication", "emergency", "radio", "reception", "signal", "waves" })] [FontAwesomeCategoriesAttribute(new[] { "Connectivity", "Energy", "Film + Video", "Humanitarian" })] BroadcastTower = 0xF519, @@ -1221,7 +1228,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "brush" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "brush", "art", "bristles", "color", "handle", "paint" })] + [FontAwesomeSearchTerms(new[] { "brush", "art", "bristles", "color", "handle", "maintenance", "modify", "paint" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Design", "Editing" })] Brush = 0xF55D, @@ -1249,7 +1256,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bug-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bug slash", "beetle", "fix", "glitch", "insect", "optimize", "repair", "report", "warning" })] + [FontAwesomeSearchTerms(new[] { "bug slash", "beetle", "disabled", "fix", "glitch", "insect", "optimize", "repair", "report", "warning" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Security" })] BugSlash = 0xE490, @@ -1270,42 +1277,42 @@ public enum FontAwesomeIcon /// /// The Font Awesome "building-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building circle check", "building", "city", "not affected", "office", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "building circle check", "building", "city", "enable", "not affected", "office", "ok", "okay", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingCircleCheck = 0xE4D2, /// /// The Font Awesome "building-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building circle exclamation", "affected", "building", "city", "office" })] + [FontAwesomeSearchTerms(new[] { "building circle exclamation", "affected", "building", "city", "failed", "office" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingCircleExclamation = 0xE4D3, /// /// The Font Awesome "building-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building circle xmark", "building", "city", "destroy", "office" })] + [FontAwesomeSearchTerms(new[] { "building circle xmark", "building", "city", "destroy", "office", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingCircleXmark = 0xE4D4, /// /// The Font Awesome "building-flag" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building flag", " city", "building", "diplomat", "embassy", "flag", "headquarters", "united nations" })] + [FontAwesomeSearchTerms(new[] { "building flag", "building", "city", "diplomat", "embassy", "flag", "headquarters", "united nations" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Political" })] BuildingFlag = 0xE4D5, /// /// The Font Awesome "building-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building lock", "building", "city", "closed", "lock", "lockdown", "quarantine", "secure" })] + [FontAwesomeSearchTerms(new[] { "building lock", "building", "city", "closed", "lock", "lockdown", "padlock", "privacy", "quarantine", "secure" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Security" })] BuildingLock = 0xE4D6, /// /// The Font Awesome "building-ngo" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building ngo", " city", "building", "non governmental organization", "office" })] + [FontAwesomeSearchTerms(new[] { "building ngo", "building", "city", "non governmental organization", "office" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingNgo = 0xE4D7, @@ -1326,7 +1333,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "building-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building user", "apartment", "building", "city" })] + [FontAwesomeSearchTerms(new[] { "building user", "apartment", "building", "city", "employee", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingUser = 0xE4DA, @@ -1340,7 +1347,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bullhorn" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bullhorn", "bullhorn", "announcement", "broadcast", "loud", "louder", "loudspeaker", "megaphone", "public address", "share" })] + [FontAwesomeSearchTerms(new[] { "bullhorn", "bullhorn", "announcement", "broadcast", "loud", "louder", "loudspeaker", "megaphone", "public address", "request", "share" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Marketing", "Political", "Shopping" })] Bullhorn = 0xF0A1, @@ -1382,10 +1389,17 @@ public enum FontAwesomeIcon /// /// The Font Awesome "business-time" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "business time", "alarm", "briefcase", "business socks", "clock", "flight of the conchords", "reminder", "wednesday" })] + [FontAwesomeSearchTerms(new[] { "business time", "alarm", "briefcase", "business socks", "clock", "flight of the conchords", "portfolio", "reminder", "wednesday" })] [FontAwesomeCategoriesAttribute(new[] { "Business" })] BusinessTime = 0xF64A, + /// + /// The Font Awesome "bus-side" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "bus side", "bus", "public transportation", "transportation", "travel", "vehicle" })] + [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Humanitarian", "Logistics", "Transportation", "Travel + Hotel" })] + BusSide = 0xE81D, + /// /// The Font Awesome "calculator" icon unicode character. /// @@ -1410,7 +1424,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "calendar-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "calendar check", "accept", "agree", "appointment", "confirm", "correct", "date", "day", "done", "event", "month", "ok", "schedule", "select", "success", "tick", "time", "todo", "when", "year" })] + [FontAwesomeSearchTerms(new[] { "calendar check", "accept", "agree", "appointment", "confirm", "correct", "date", "day", "done", "enable", "event", "month", "ok", "schedule", "select", "success", "tick", "time", "todo", "validate", "warranty", "when", "working", "year" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] CalendarCheck = 0xF274, @@ -1438,7 +1452,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "calendar-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "calendar xmark", "archive", "calendar", "date", "day", "delete", "event", "month", "remove", "schedule", "time", "when", "x", "year" })] + [FontAwesomeSearchTerms(new[] { "calendar xmark", "archive", "calendar", "date", "day", "delete", "event", "month", "remove", "schedule", "time", "uncheck", "when", "x", "year" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] CalendarTimes = 0xF273, @@ -1452,21 +1466,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "camera" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "camera", "image", "lens", "photo", "picture", "record", "shutter", "video" })] + [FontAwesomeSearchTerms(new[] { "camera", "image", "img", "lens", "photo", "picture", "record", "shutter", "video" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware", "Photos + Images", "Shopping", "Social" })] Camera = 0xF030, /// /// The Font Awesome "camera-retro" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "camera retro", "camera", "image", "lens", "photo", "picture", "record", "shutter", "video" })] + [FontAwesomeSearchTerms(new[] { "camera retro", "camera", "image", "img", "lens", "photo", "picture", "record", "shutter", "video" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware", "Photos + Images", "Shopping" })] CameraRetro = 0xF083, /// /// The Font Awesome "camera-rotate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "camera rotate", "flip", "front-facing", "photo", "selfie" })] + [FontAwesomeSearchTerms(new[] { "camera rotate", "flip", "front-facing", "img", "photo", "selfie" })] [FontAwesomeCategoriesAttribute(new[] { "Photos + Images" })] CameraRotate = 0xE0D8, @@ -1557,7 +1571,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-caret-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square caret down", "arrow", "caret-square-o-down", "dropdown", "expand", "menu", "more", "triangle" })] + [FontAwesomeSearchTerms(new[] { "square caret down", "arrow", "caret-square-o-down", "dropdown", "expand", "insert", "menu", "more", "triangle" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] CaretSquareDown = 0xF150, @@ -1578,14 +1592,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-caret-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square caret up", "arrow", "caret-square-o-up", "collapse", "triangle", "upload" })] + [FontAwesomeSearchTerms(new[] { "square caret up", "arrow", "caret-square-o-up", "collapse", "triangle", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] CaretSquareUp = 0xF151, /// /// The Font Awesome "caret-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "caret up", "arrow", "collapse", "triangle" })] + [FontAwesomeSearchTerms(new[] { "caret up", "arrow", "collapse", "triangle", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] CaretUp = 0xF0D8, @@ -1613,7 +1627,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "cart-arrow-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "cart arrow down", "download", "save", "shopping" })] + [FontAwesomeSearchTerms(new[] { "cart arrow down", "download", "insert", "save", "shopping" })] [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] CartArrowDown = 0xF218, @@ -1662,7 +1676,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "certificate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "certificate", "badge", "star", "verified" })] + [FontAwesomeSearchTerms(new[] { "certificate", "badge", "guarantee", "star", "verified" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Shapes", "Shopping", "Spinners" })] Certificate = 0xF0A3, @@ -1683,91 +1697,98 @@ public enum FontAwesomeIcon /// /// The Font Awesome "chalkboard-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chalkboard user", "blackboard", "instructor", "learning", "professor", "school", "whiteboard", "writing" })] + [FontAwesomeSearchTerms(new[] { "chalkboard user", "blackboard", "instructor", "learning", "professor", "school", "uer", "whiteboard", "writing" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Users + People" })] ChalkboardTeacher = 0xF51C, /// /// The Font Awesome "charging-station" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "charging station", "electric", "ev", "tesla", "vehicle" })] + [FontAwesomeSearchTerms(new[] { "charging station", "car charger", "charge", "charging", "electric", "ev", "tesla", "vehicle" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Energy" })] ChargingStation = 0xF5E7, /// /// The Font Awesome "chart-area" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart area", "analytics", "area", "chart", "graph" })] + [FontAwesomeSearchTerms(new[] { "chart area", "analytics", "area", "chart", "graph", "performance", "revenue", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams" })] ChartArea = 0xF1FE, /// /// The Font Awesome "chart-bar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart bar", "analytics", "bar", "chart", "graph" })] + [FontAwesomeSearchTerms(new[] { "chart bar", "analytics", "bar", "chart", "graph", "performance", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams" })] ChartBar = 0xF080, /// /// The Font Awesome "chart-column" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart column", "bar", "bar chart", "chart", "graph", "track", "trend" })] + [FontAwesomeSearchTerms(new[] { "chart column", "bar", "bar chart", "chart", "graph", "performance", "revenue", "statistics", "track", "trend" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams" })] ChartColumn = 0xE0E3, + /// + /// The Font Awesome "chart-diagram" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "chart diagram", "algorithm", "analytics", "flow", "graph" })] + [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Coding" })] + ChartDiagram = 0xE695, + /// /// The Font Awesome "chart-gantt" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart gantt", "chart", "graph", "track", "trend" })] + [FontAwesomeSearchTerms(new[] { "chart gantt", "chart", "graph", "performance", "statistics", "track", "trend" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams" })] ChartGantt = 0xE0E4, /// /// The Font Awesome "chart-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart line", "activity", "analytics", "chart", "dashboard", "gain", "graph", "increase", "line" })] + [FontAwesomeSearchTerms(new[] { "chart line", "activity", "analytics", "chart", "dashboard", "gain", "graph", "increase", "line", "performance", "revenue", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Money" })] ChartLine = 0xF201, /// /// The Font Awesome "chart-pie" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart pie", "analytics", "chart", "diagram", "graph", "pie" })] + [FontAwesomeSearchTerms(new[] { "chart pie", "analytics", "chart", "diagram", "graph", "performance", "pie", "revenue", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Money" })] ChartPie = 0xF200, /// /// The Font Awesome "chart-simple" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart simple", "analytics", "bar", "chart", "column", "graph", "row", "trend" })] + [FontAwesomeSearchTerms(new[] { "chart simple", "analytics", "bar", "chart", "column", "graph", "performance", "revenue", "row", "statistics", "trend" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Editing", "Logistics", "Marketing" })] ChartSimple = 0xE473, /// /// The Font Awesome "check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "check mark", "accept", "agree", "check", "check mark", "checkmark", "confirm", "correct", "done", "mark", "notice", "notification", "notify", "ok", "select", "success", "tick", "todo", "yes", "✓" })] + [FontAwesomeSearchTerms(new[] { "check mark", "accept", "agree", "check", "check mark", "checkmark", "confirm", "correct", "coupon", "done", "enable", "mark", "notice", "notification", "notify", "ok", "select", "success", "tick", "todo", "true", "validate", "working", "yes", "✓" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Punctuation + Symbols", "Text Formatting" })] Check = 0xF00C, /// /// The Font Awesome "circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle check", "accept", "affected", "agree", "clear", "confirm", "correct", "done", "ok", "select", "success", "tick", "todo", "yes" })] + [FontAwesomeSearchTerms(new[] { "circle check", "accept", "affected", "agree", "clear", "confirm", "correct", "coupon", "done", "enable", "ok", "select", "success", "tick", "todo", "validate", "working", "yes" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Text Formatting", "Toggle" })] CheckCircle = 0xF058, /// /// The Font Awesome "check-double" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "check double", "accept", "agree", "checkmark", "confirm", "correct", "done", "notice", "notification", "notify", "ok", "select", "success", "tick", "todo" })] + [FontAwesomeSearchTerms(new[] { "check double", "accept", "agree", "checkmark", "confirm", "correct", "coupon", "done", "enable", "notice", "notification", "notify", "ok", "select", "select all", "success", "tick", "todo", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Political", "Punctuation + Symbols", "Text Formatting" })] CheckDouble = 0xF560, /// /// The Font Awesome "square-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square check", "accept", "agree", "box", "button", "check", "check box with check", "check mark button", "checkmark", "confirm", "correct", "done", "mark", "ok", "select", "success", "tick", "todo", "yes", "✓" })] + [FontAwesomeSearchTerms(new[] { "square check", "accept", "agree", "box", "button", "check", "check box with check", "check mark button", "checkmark", "confirm", "correct", "coupon", "done", "enable", "mark", "ok", "select", "success", "tick", "todo", "validate", "working", "yes", "✓" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Text Formatting" })] CheckSquare = 0xF14A, @@ -1858,14 +1879,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "circle-chevron-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle chevron up", "arrow", "collapse", "upload" })] + [FontAwesomeSearchTerms(new[] { "circle chevron up", "arrow", "collapse", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ChevronCircleUp = 0xF139, /// /// The Font Awesome "chevron-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chevron down", "arrow", "download", "expand" })] + [FontAwesomeSearchTerms(new[] { "chevron down", "arrow", "download", "expand", "insert" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ChevronDown = 0xF078, @@ -1886,14 +1907,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "chevron-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chevron up", "arrow", "collapse", "upload" })] + [FontAwesomeSearchTerms(new[] { "chevron up", "arrow", "collapse", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ChevronUp = 0xF077, /// /// The Font Awesome "child" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "child", "boy", "girl", "kid", "toddler", "young", "youth" })] + [FontAwesomeSearchTerms(new[] { "child", "boy", "girl", "kid", "toddler", "uer", "young", "youth" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Users + People" })] Child = 0xF1AE, @@ -1907,21 +1928,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "child-dress" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "child dress", "boy", "girl", "kid", "toddler", "young", "youth" })] + [FontAwesomeSearchTerms(new[] { "child dress", "boy", "girl", "kid", "toddler", "uer", "young", "youth" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Users + People" })] ChildDress = 0xE59C, /// /// The Font Awesome "child-reaching" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "child reaching", "boy", "girl", "kid", "toddler", "young", "youth" })] + [FontAwesomeSearchTerms(new[] { "child reaching", "boy", "girl", "kid", "toddler", "uer", "young", "youth" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Users + People" })] ChildReaching = 0xE59D, /// /// The Font Awesome "children" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "children", "boy", "child", "girl", "kid", "kids", "young", "youth" })] + [FontAwesomeSearchTerms(new[] { "children", "boy", "child", "girl", "kid", "kids", "together", "uer", "young", "youth" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Humanitarian", "Users + People" })] Children = 0xE4E1, @@ -1977,49 +1998,49 @@ public enum FontAwesomeIcon /// /// The Font Awesome "clipboard" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboar", "clipboard", "copy", "notes", "paste", "record" })] + [FontAwesomeSearchTerms(new[] { "clipboard", "copy", "notepad", "notes", "paste", "record" })] [FontAwesomeCategoriesAttribute(new[] { "Business" })] Clipboard = 0xF328, /// /// The Font Awesome "clipboard-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboard check", "accept", "agree", "confirm", "done", "ok", "select", "success", "tick", "todo", "yes" })] - [FontAwesomeCategoriesAttribute(new[] { "Logistics", "Science" })] + [FontAwesomeSearchTerms(new[] { "clipboard check", "accept", "agree", "confirm", "coupon", "done", "enable", "ok", "select", "success", "tick", "todo", "validate", "working", "yes" })] + [FontAwesomeCategoriesAttribute(new[] { "Business", "Logistics", "Science" })] ClipboardCheck = 0xF46C, /// /// The Font Awesome "clipboard-list" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboard list", "checklist", "completed", "done", "finished", "intinerary", "ol", "schedule", "tick", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "clipboard list", "cheatsheet", "checklist", "completed", "done", "finished", "intinerary", "ol", "schedule", "summary", "survey", "tick", "todo", "ul", "wishlist" })] [FontAwesomeCategoriesAttribute(new[] { "Logistics" })] ClipboardList = 0xF46D, /// /// The Font Awesome "clipboard-question" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboard question", "assistance", "interview", "query", "question" })] + [FontAwesomeSearchTerms(new[] { "clipboard question", "assistance", "faq", "interview", "query", "question" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Humanitarian", "Logistics" })] ClipboardQuestion = 0xE4E3, /// /// The Font Awesome "clipboard-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboard user", "attendance", "record", "roster", "staff" })] - [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Users + People" })] + [FontAwesomeSearchTerms(new[] { "clipboard user", "attendance", "employee", "record", "roster", "staff", "uer" })] + [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Medical + Health", "Users + People" })] ClipboardUser = 0xF7F3, /// /// The Font Awesome "clock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "00", "4", "4:00", "clock", "date", "four", "four o’clock", "hour", "late", "minute", "o'clock", "o’clock", "schedule", "ticking", "time", "timer", "timestamp", "watch" })] + [FontAwesomeSearchTerms(new[] { "00", "4", "4:00", "clock", "date", "four", "four o’clock", "hour", "late", "minute", "o'clock", "o’clock", "pending", "schedule", "ticking", "time", "timer", "timestamp", "watch" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] Clock = 0xF017, /// /// The Font Awesome "clone" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clone", "arrange", "copy", "duplicate", "paste" })] + [FontAwesomeSearchTerms(new[] { "clone", "add", "arrange", "copy", "duplicate", "new", "paste" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Files", "Photos + Images" })] Clone = 0xF24D, @@ -2112,7 +2133,7 @@ public enum FontAwesomeIcon /// The Font Awesome "cloud-arrow-up" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF0EE. ///
- [FontAwesomeSearchTerms(new[] { "cloud arrow up", "import", "save", "upload" })] + [FontAwesomeSearchTerms(new[] { "cloud arrow up", "import", "save", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Connectivity" })] CloudUploadAlt = 0xF382, @@ -2133,14 +2154,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "code" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "brackets", "code", "development", "html" })] + [FontAwesomeSearchTerms(new[] { "brackets", "code", "development", "html", "mysql", "sql" })] [FontAwesomeCategoriesAttribute(new[] { "Coding" })] Code = 0xF121, /// /// The Font Awesome "code-branch" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "code branch", "branch", "git", "github", "rebase", "svn", "vcs", "version" })] + [FontAwesomeSearchTerms(new[] { "code branch", "branch", "git", "github", "mysql", "rebase", "sql", "svn", "vcs", "version" })] [FontAwesomeCategoriesAttribute(new[] { "Coding" })] CodeBranch = 0xF126, @@ -2189,21 +2210,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "gear" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "cog", "cogwheel", "gear", "mechanical", "settings", "sprocket", "tool", "wheel" })] + [FontAwesomeSearchTerms(new[] { "cog", "cogwheel", "configuration", "gear", "mechanical", "modify", "settings", "sprocket", "tool", "wheel" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Editing", "Spinners" })] Cog = 0xF013, /// /// The Font Awesome "gears" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "gears", "mechanical", "settings", "sprocket", "wheel" })] + [FontAwesomeSearchTerms(new[] { "configuration", "gears", "mechanical", "modify", "settings", "sprocket", "wheel" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Logistics" })] Cogs = 0xF085, /// /// The Font Awesome "coins" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "coins", "currency", "dime", "financial", "gold", "money", "penny" })] + [FontAwesomeSearchTerms(new[] { "coins", "currency", "dime", "financial", "gold", "money", "penny", "premium" })] [FontAwesomeCategoriesAttribute(new[] { "Money" })] Coins = 0xF51E, @@ -2217,63 +2238,70 @@ public enum FontAwesomeIcon /// /// The Font Awesome "table-columns" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table columns", "browser", "dashboard", "organize", "panes", "split" })] + [FontAwesomeSearchTerms(new[] { "table columns", "browser", "category", "dashboard", "organize", "panes", "split" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Text Formatting" })] Columns = 0xF0DB, /// /// The Font Awesome "comment" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment", "right speech bubble", "bubble", "chat", "commenting", "conversation", "feedback", "message", "note", "notification", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment", "right speech bubble", "answer", "bubble", "chat", "commenting", "conversation", "conversation", "discussion", "feedback", "message", "note", "notification", "sms", "speech", "talk", "talking", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Shapes", "Social" })] Comment = 0xF075, /// /// The Font Awesome "message" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bubble", "chat", "commenting", "conversation", "feedback", "message", "note", "notification", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "answer", "bubble", "chat", "commenting", "conversation", "conversation", "discussion", "feedback", "message", "note", "notification", "sms", "speech", "talk", "talking", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Social" })] CommentAlt = 0xF27A, /// /// The Font Awesome "comment-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment dollar", "bubble", "chat", "commenting", "conversation", "feedback", "message", "money", "note", "notification", "pay", "sms", "speech", "spend", "texting", "transfer" })] + [FontAwesomeSearchTerms(new[] { "comment dollar", "answer", "bubble", "chat", "commenting", "conversation", "feedback", "message", "money", "note", "notification", "pay", "salary", "sms", "speech", "spend", "texting", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing", "Money" })] CommentDollar = 0xF651, /// /// The Font Awesome "comment-dots" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment dots", "balloon", "bubble", "chat", "comic", "commenting", "conversation", "dialog", "feedback", "message", "more", "note", "notification", "reply", "sms", "speech", "speech balloon", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment dots", "answer", "balloon", "bubble", "chat", "comic", "commenting", "conversation", "dialog", "feedback", "message", "more", "note", "notification", "reply", "request", "sms", "speech", "speech balloon", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication" })] CommentDots = 0xF4AD, /// /// The Font Awesome "comment-medical" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment medical", "advice", "bubble", "chat", "commenting", "conversation", "diagnose", "feedback", "message", "note", "notification", "prescription", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment medical", "advice", "answer", "bubble", "chat", "commenting", "conversation", "diagnose", "feedback", "message", "note", "notification", "prescription", "sms", "speech", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Medical + Health" })] CommentMedical = 0xF7F5, + /// + /// The Font Awesome "comment-nodes" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "comment nodes", "ai", "artificial intelligence", "cluster", "language", "model", "network", "neuronal" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Communication" })] + CommentNodes = 0xE696, + /// /// The Font Awesome "comments" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comments", "two speech bubbles", "bubble", "chat", "commenting", "conversation", "feedback", "message", "note", "notification", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "comments", "two speech bubbles", "answer", "bubble", "chat", "commenting", "conversation", "conversation", "discussion", "feedback", "message", "note", "notification", "sms", "speech", "talk", "talking", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication" })] Comments = 0xF086, /// /// The Font Awesome "comments-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comments dollar", "bubble", "chat", "commenting", "conversation", "feedback", "message", "money", "note", "notification", "pay", "sms", "speech", "spend", "texting", "transfer" })] + [FontAwesomeSearchTerms(new[] { "comments dollar", "answer", "bubble", "chat", "commenting", "conversation", "feedback", "message", "money", "note", "notification", "pay", "salary", "sms", "speech", "spend", "texting", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing", "Money" })] CommentsDollar = 0xF653, /// /// The Font Awesome "comment-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment slash", "bubble", "cancel", "chat", "commenting", "conversation", "feedback", "message", "mute", "note", "notification", "quiet", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment slash", "answer", "bubble", "cancel", "chat", "commenting", "conversation", "disabled", "feedback", "message", "mute", "note", "notification", "quiet", "sms", "speech", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication" })] CommentSlash = 0xF4B3, @@ -2301,7 +2329,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "down-left-and-up-right-to-center" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "down left and up right to center", "collapse", "fullscreen", "minimize", "move", "resize", "shrink", "smaller" })] + [FontAwesomeSearchTerms(new[] { "down left and up right to center", "collapse", "fullscreen", "minimize", "move", "resize", "scale", "shrink", "size", "smaller" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] CompressAlt = 0xF422, @@ -2322,7 +2350,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bell-concierge" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bell concierge", "attention", "bell", "bellhop", "bellhop bell", "hotel", "receptionist", "service", "support" })] + [FontAwesomeSearchTerms(new[] { "bell concierge", "attention", "bell", "bellhop", "bellhop bell", "hotel", "receptionist", "request", "service", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel" })] ConciergeBell = 0xF562, @@ -2378,14 +2406,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "crop" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "crop", "design", "frame", "mask", "resize", "shrink" })] + [FontAwesomeSearchTerms(new[] { "crop", "design", "frame", "mask", "modify", "resize", "shrink" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Editing" })] Crop = 0xF125, /// /// The Font Awesome "crop-simple" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "crop simple", "design", "frame", "mask", "resize", "shrink" })] + [FontAwesomeSearchTerms(new[] { "crop simple", "design", "frame", "mask", "modify", "resize", "shrink" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Editing" })] CropAlt = 0xF565, @@ -2413,7 +2441,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "crown" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "award", "clothing", "crown", "favorite", "king", "queen", "royal", "tiara" })] + [FontAwesomeSearchTerms(new[] { "award", "clothing", "crown", "favorite", "king", "queen", "royal", "tiara", "vip" })] [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] Crown = 0xF521, @@ -2455,14 +2483,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "scissors" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "black safety scissors", "white scissors", "clip", "cutting", "scissors", "snip", "tool" })] + [FontAwesomeSearchTerms(new[] { "black safety scissors", "white scissors", "clip", "cutting", "equipment", "modify", "scissors", "snip", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing", "Files" })] Cut = 0xF0C4, /// /// The Font Awesome "database" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "database", "computer", "development", "directory", "memory", "storage" })] + [FontAwesomeSearchTerms(new[] { "database", "computer", "development", "directory", "memory", "mysql", "sql", "storage" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] Database = 0xF1C0, @@ -2470,7 +2498,7 @@ public enum FontAwesomeIcon /// The Font Awesome "ear-deaf" icon unicode character. ///
[FontAwesomeSearchTerms(new[] { "ear deaf", "ear", "hearing", "sign language" })] - [FontAwesomeCategoriesAttribute(new[] { "Accessibility" })] + [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Communication" })] Deaf = 0xF2A4, /// @@ -2498,7 +2526,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-dots-from-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person dots from line", "allergy", "diagnosis" })] + [FontAwesomeSearchTerms(new[] { "person dots from line", "allergy", "diagnosis", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] Diagnoses = 0xF470, @@ -2526,7 +2554,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "diamond" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "diamond", "card", "cards", "diamond suit", "game", "gem", "gemstone", "poker", "suit" })] + [FontAwesomeSearchTerms(new[] { "diamond", "ace", "card", "cards", "diamond suit", "game", "gem", "gemstone", "poker", "suit" })] [FontAwesomeCategoriesAttribute(new[] { "Gaming", "Shapes" })] Diamond = 0xF219, @@ -2653,7 +2681,7 @@ public enum FontAwesomeIcon /// The Font Awesome "dollar-sign" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x24. /// - [FontAwesomeSearchTerms(new[] { "dollar sign", "dollar sign", "currency", "dollar", "heavy dollar sign", "money" })] + [FontAwesomeSearchTerms(new[] { "dollar sign", "dollar sign", "coupon", "currency", "dollar", "heavy dollar sign", "investment", "money", "premium", "revenue", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Maps", "Money" })] DollarSign = 0xF155, @@ -2674,7 +2702,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "circle-dollar-to-slot" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle dollar to slot", "contribute", "generosity", "gift", "give" })] + [FontAwesomeSearchTerms(new[] { "circle dollar to slot", "contribute", "generosity", "gift", "give", "premium" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Money", "Political" })] Donate = 0xF4B9, @@ -2688,7 +2716,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "door-closed" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "door closed", "doo", "door", "enter", "exit", "locked" })] + [FontAwesomeSearchTerms(new[] { "door closed", "doo", "door", "enter", "exit", "locked", "privacy" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Security", "Travel + Hotel" })] DoorClosed = 0xF52A, @@ -2716,7 +2744,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "download" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "download", "export", "hard drive", "save", "transfer" })] + [FontAwesomeSearchTerms(new[] { "download", "export", "hard drive", "insert", "save", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Devices + Hardware" })] Download = 0xF019, @@ -2765,7 +2793,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "dumbbell" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "dumbbell", "exercise", "gym", "strength", "weight", "weight-lifting" })] + [FontAwesomeSearchTerms(new[] { "dumbbell", "exercise", "gym", "strength", "weight", "weight-lifting", "workout" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Travel + Hotel" })] Dumbbell = 0xF44B, @@ -2800,7 +2828,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "pen-to-square" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen to square", "edit", "pen", "pencil", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "pen to square", "edit", "modify", "pen", "pencil", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing", "Writing" })] Edit = 0xF044, @@ -2821,56 +2849,56 @@ public enum FontAwesomeIcon /// /// The Font Awesome "elevator" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "accessibility", "elevator", "hoist", "lift", "users-people" })] + [FontAwesomeSearchTerms(new[] { "accessibility", "elevator", "hoist", "lift", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel", "Users + People" })] Elevator = 0xE16D, /// /// The Font Awesome "ellipsis" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ellipsis", "dots", "drag", "kebab", "list", "menu", "nav", "navigation", "ol", "pacman", "reorder", "settings", "ul" })] + [FontAwesomeSearchTerms(new[] { "ellipsis", "dots", "drag", "kebab", "list", "menu", "nav", "navigation", "ol", "pacman", "reorder", "settings", "three dots", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] EllipsisH = 0xF141, /// /// The Font Awesome "ellipsis-vertical" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ellipsis vertical", "dots", "drag", "kebab", "list", "menu", "nav", "navigation", "ol", "reorder", "settings", "ul" })] + [FontAwesomeSearchTerms(new[] { "ellipsis vertical", "bullet", "dots", "drag", "kebab", "list", "menu", "nav", "navigation", "ol", "reorder", "settings", "three dots", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] EllipsisV = 0xF142, /// /// The Font Awesome "envelope" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "back of envelope", "e-mail", "email", "envelope", "letter", "mail", "message", "notification", "support" })] + [FontAwesomeSearchTerms(new[] { "back of envelope", "e-mail", "email", "envelope", "letter", "mail", "message", "newsletter", "notification", "offer", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Humanitarian", "Social", "Writing" })] Envelope = 0xF0E0, /// /// The Font Awesome "envelope-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "envelope circle check", "check", "email", "envelope", "mail", "not affected", "ok", "okay", "read", "sent" })] + [FontAwesomeSearchTerms(new[] { "envelope circle check", "check", "email", "enable", "envelope", "mail", "not affected", "ok", "okay", "read", "sent", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Humanitarian" })] EnvelopeCircleCheck = 0xE4E8, /// /// The Font Awesome "envelope-open" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "envelope open", "e-mail", "email", "letter", "mail", "message", "notification", "support" })] + [FontAwesomeSearchTerms(new[] { "envelope open", "e-mail", "email", "letter", "mail", "message", "newsletter", "notification", "offer", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Writing" })] EnvelopeOpen = 0xF2B6, /// /// The Font Awesome "envelope-open-text" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "envelope open text", "e-mail", "email", "letter", "mail", "message", "notification", "support" })] + [FontAwesomeSearchTerms(new[] { "envelope open text", "e-mail", "email", "letter", "mail", "message", "newsletter", "notification", "offer", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] EnvelopeOpenText = 0xF658, /// /// The Font Awesome "square-envelope" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square envelope", "e-mail", "email", "letter", "mail", "message", "notification", "support" })] + [FontAwesomeSearchTerms(new[] { "square envelope", "e-mail", "email", "letter", "mail", "message", "notification", "offer", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication" })] EnvelopeSquare = 0xF199, @@ -2914,42 +2942,42 @@ public enum FontAwesomeIcon /// The Font Awesome "exclamation" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x21. ///
- [FontAwesomeSearchTerms(new[] { "!", "exclamation mark", "alert", "danger", "error", "exclamation", "important", "mark", "notice", "notification", "notify", "outlined", "problem", "punctuation", "red exclamation mark", "warning", "white exclamation mark" })] + [FontAwesomeSearchTerms(new[] { "!", "exclamation mark", "alert", "attention", "danger", "error", "exclamation", "failed", "important", "mark", "notice", "notification", "notify", "outlined", "problem", "punctuation", "red exclamation mark", "required", "warning", "white exclamation mark" })] [FontAwesomeCategoriesAttribute(new[] { "Alert", "Punctuation + Symbols" })] Exclamation = 0xF12A, /// /// The Font Awesome "circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle exclamation", "affect", "alert", "damage", "danger", "error", "important", "notice", "notification", "notify", "problem", "warning" })] + [FontAwesomeSearchTerms(new[] { "circle exclamation", "affect", "alert", "attention", "damage", "danger", "error", "failed", "important", "notice", "notification", "notify", "problem", "required", "warning" })] [FontAwesomeCategoriesAttribute(new[] { "Alert", "Punctuation + Symbols" })] ExclamationCircle = 0xF06A, /// /// The Font Awesome "triangle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "triangle exclamation", "alert", "danger", "error", "important", "notice", "notification", "notify", "problem", "warnin", "warning" })] + [FontAwesomeSearchTerms(new[] { "triangle exclamation", "alert", "attention", "danger", "error", "failed", "important", "notice", "notification", "notify", "problem", "required", "warnin", "warning" })] [FontAwesomeCategoriesAttribute(new[] { "Alert" })] ExclamationTriangle = 0xF071, /// /// The Font Awesome "expand" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "expand", "bigger", "crop", "enlarge", "focus", "fullscreen", "resize", "viewfinder" })] + [FontAwesomeSearchTerms(new[] { "arrows", "bigger", "enlarge", "expand", "fullscreen", "maximize", "resize", "resize", "scale", "size", "viewfinder" })] [FontAwesomeCategoriesAttribute(new[] { "Media Playback" })] Expand = 0xF065, /// /// The Font Awesome "up-right-and-down-left-from-center" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "up right and down left from center", "arrows", "bigger", "enlarge", "fullscreen", "resize" })] + [FontAwesomeSearchTerms(new[] { "up right and down left from center", "arrows", "bigger", "enlarge", "expand", "fullscreen", "maximize", "resize", "resize", "scale", "size" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] ExpandAlt = 0xF424, /// /// The Font Awesome "maximize" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "maximize", "bigger", "enlarge", "fullscreen", "move", "resize" })] + [FontAwesomeSearchTerms(new[] { "arrows", "bigger", "enlarge", "expand", "fullscreen", "maximize", "resize", "resize", "scale", "size" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] ExpandArrowsAlt = 0xF31E, @@ -2963,7 +2991,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "up-right-from-square" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "up right from square", "external-link", "new", "open", "share" })] + [FontAwesomeSearchTerms(new[] { "up right from square", "external-link", "new", "open", "share", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ExternalLinkAlt = 0xF35D, @@ -2991,7 +3019,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "eye-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "eye slash", "blind", "hide", "show", "toggle", "unseen", "views", "visible", "visiblity" })] + [FontAwesomeSearchTerms(new[] { "eye slash", "blind", "disabled", "hide", "show", "toggle", "unseen", "views", "visible", "visiblity" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Editing", "Maps", "Photos + Images", "Security" })] EyeSlash = 0xF070, @@ -3005,14 +3033,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "backward-fast" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "backward fast", "arrow", "beginning", "first", "last track button", "previous", "previous scene", "previous track", "rewind", "start", "triangle" })] + [FontAwesomeSearchTerms(new[] { "backward fast", "arrow", "beginning", "first", "last track button", "previous", "previous scene", "previous track", "quick", "rewind", "start", "triangle" })] [FontAwesomeCategoriesAttribute(new[] { "Media Playback" })] FastBackward = 0xF049, /// /// The Font Awesome "forward-fast" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "forward fast", "arrow", "end", "last", "next", "next scene", "next track", "next track button", "triangle" })] + [FontAwesomeSearchTerms(new[] { "forward fast", "arrow", "end", "last", "next", "next scene", "next track", "next track button", "quick", "triangle" })] [FontAwesomeCategoriesAttribute(new[] { "Media Playback" })] FastForward = 0xF050, @@ -3054,7 +3082,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-dress" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person dress", "man", "skirt", "woman" })] + [FontAwesomeSearchTerms(new[] { "person dress", "man", "skirt", "uer", "woman" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] Female = 0xF182, @@ -3075,7 +3103,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file", "empty document", "document", "new", "page", "page facing up", "pdf", "resume" })] + [FontAwesomeSearchTerms(new[] { "file", "empty document", "cv", "document", "new", "page", "page facing up", "pdf", "resume" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Coding", "Files", "Humanitarian", "Shapes", "Writing" })] File = 0xF15B, @@ -3103,14 +3131,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file circle check", "document", "file", "not affected", "ok", "okay", "paper" })] + [FontAwesomeSearchTerms(new[] { "file circle check", "document", "enable", "file", "not affected", "ok", "okay", "paper", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Humanitarian" })] FileCircleCheck = 0xE5A0, /// /// The Font Awesome "file-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file circle exclamation", "document", "file", "paper" })] + [FontAwesomeSearchTerms(new[] { "file circle exclamation", "document", "failed", "file", "paper" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Humanitarian" })] FileCircleExclamation = 0xE4EB, @@ -3138,21 +3166,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file circle xmark", "document", "file", "paper" })] + [FontAwesomeSearchTerms(new[] { "file circle xmark", "document", "file", "paper", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Humanitarian" })] FileCircleXmark = 0xE5A1, /// /// The Font Awesome "file-code" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file code", "css", "development", "document", "html" })] + [FontAwesomeSearchTerms(new[] { "file code", "css", "development", "document", "html", "mysql", "sql" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Files" })] FileCode = 0xF1C9, /// /// The Font Awesome "file-contract" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file contract", "agreement", "binding", "document", "legal", "signature" })] + [FontAwesomeSearchTerms(new[] { "file contract", "agreement", "binding", "document", "legal", "signature", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] FileContract = 0xF56C, @@ -3166,7 +3194,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-arrow-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file arrow down", "document", "export", "save" })] + [FontAwesomeSearchTerms(new[] { "file arrow down", "archive", "document", "export", "insert", "save" })] [FontAwesomeCategoriesAttribute(new[] { "Files" })] FileDownload = 0xF56D, @@ -3184,17 +3212,31 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Files" })] FileExport = 0xF56E, + /// + /// The Font Awesome "file-fragment" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "file fragment", "block", "data", "partial", "piece" })] + [FontAwesomeCategoriesAttribute(new[] { "Files" })] + FileFragment = 0xE697, + + /// + /// The Font Awesome "file-half-dashed" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "file half dashed", "data", "fragment", "partial", "piece" })] + [FontAwesomeCategoriesAttribute(new[] { "Files" })] + FileHalfDashed = 0xE698, + /// /// The Font Awesome "file-image" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file image", "document with picture", "document", "image", "jpg", "photo", "png" })] + [FontAwesomeSearchTerms(new[] { "file image", "document with picture", "document", "image", "img", "jpg", "photo", "png" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Photos + Images" })] FileImage = 0xF1C5, /// /// The Font Awesome "file-import" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file import", "copy", "document", "send", "upload" })] + [FontAwesomeSearchTerms(new[] { "file import", "copy", "document", "insert", "send", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Files" })] FileImport = 0xF56F, @@ -3208,7 +3250,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-invoice-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file invoice dollar", "$", "account", "bill", "charge", "document", "dollar-sign", "money", "payment", "receipt", "usd" })] + [FontAwesomeSearchTerms(new[] { "file invoice dollar", "$", "account", "bill", "charge", "document", "dollar-sign", "money", "payment", "receipt", "revenue", "salary", "usd" })] [FontAwesomeCategoriesAttribute(new[] { "Money" })] FileInvoiceDollar = 0xF571, @@ -3236,7 +3278,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-pen" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file pen", "edit", "memo", "pen", "pencil", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "file pen", "edit", "memo", "modify", "pen", "pencil", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Humanitarian" })] FilePen = 0xF31C, @@ -3264,14 +3306,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-signature" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file signature", "john hancock", "contract", "document", "name" })] + [FontAwesomeSearchTerms(new[] { "file signature", "john hancock", "contract", "document", "name", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] FileSignature = 0xF573, /// /// The Font Awesome "file-arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file arrow up", "document", "import", "page", "save" })] + [FontAwesomeSearchTerms(new[] { "file arrow up", "document", "import", "page", "save", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Files" })] FileUpload = 0xF574, @@ -3320,14 +3362,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "filter-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "filter circle xmark", "cancel", "funnel", "options", "remove", "separate", "sort" })] + [FontAwesomeSearchTerms(new[] { "filter circle xmark", "cancel", "funnel", "options", "remove", "separate", "sort", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] FilterCircleXmark = 0xE17B, /// /// The Font Awesome "fingerprint" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "fingerprint", "human", "id", "identification", "lock", "smudge", "touch", "unique", "unlock" })] + [FontAwesomeSearchTerms(new[] { "fingerprint", "human", "id", "identification", "lock", "privacy", "smudge", "touch", "unique", "unlock" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Security" })] Fingerprint = 0xF577, @@ -3411,14 +3453,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "flask" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "flask", "beaker", "chemicals", "experiment", "experimental", "labs", "liquid", "potion", "science", "vial" })] + [FontAwesomeSearchTerms(new[] { "flask", "beaker", "chemicals", "experiment", "experimental", "knowledge", "labs", "liquid", "potion", "science", "vial" })] [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage", "Maps", "Medical + Health", "Science" })] Flask = 0xF0C3, /// /// The Font Awesome "flask-vial" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "flask vial", " beaker", " chemicals", " experiment", " experimental", " labs", " liquid", " science", " vial", "ampule", "chemistry", "lab", "laboratory", "potion", "test", "test tube" })] + [FontAwesomeSearchTerms(new[] { "flask vial", "ampule", "beaker", "chemicals", "chemistry", "experiment", "experimental", "lab", "laboratory", "labs", "liquid", "potion", "science", "test", "test tube", "vial" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Science" })] FlaskVial = 0xE4F3, @@ -3539,7 +3581,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "face-frown" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "face frown", "disapprove", "emoticon", "face", "frown", "frowning face", "rating", "sad" })] + [FontAwesomeSearchTerms(new[] { "face frown", "disapprove", "emoticon", "face", "frown", "frowning face", "rating", "sad", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Emoji", "Users + People" })] Frown = 0xF119, @@ -3553,7 +3595,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "filter-circle-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "filter circle dollar", "filter", "money", "options", "separate", "sort" })] + [FontAwesomeSearchTerms(new[] { "filter circle dollar", "filter", "money", "options", "premium", "separate", "sort" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] FunnelDollar = 0xF662, @@ -3567,7 +3609,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "gamepad" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "gamepad", "arcade", "controller", "d-pad", "joystick", "video", "video game" })] + [FontAwesomeSearchTerms(new[] { "gamepad", "arcade", "controller", "d-pad", "joystick", "playstore", "video", "video game" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Devices + Hardware", "Gaming", "Maps" })] Gamepad = 0xF11B, @@ -3595,7 +3637,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "gauge-simple-high" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "gauge simple high", "dashboard", "fast", "odometer", "speed", "speedometer" })] + [FontAwesomeSearchTerms(new[] { "gauge simple high", "dashboard", "fast", "odometer", "quick", "speed", "speedometer" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive" })] GaugeSimpleHigh = 0xF62A, @@ -3693,7 +3735,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "globe" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "all", "coordinates", "country", "earth", "global", "globe", "globe with meridians", "gps", "internet", "language", "localize", "location", "map", "meridians", "network", "online", "place", "planet", "translate", "travel", "world" })] + [FontAwesomeSearchTerms(new[] { "all", "coordinates", "country", "earth", "global", "globe", "globe with meridians", "gps", "internet", "language", "localize", "location", "map", "meridians", "network", "online", "place", "planet", "translate", "travel", "world", "www" })] [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Business", "Charity", "Connectivity", "Maps" })] Globe = 0xF0AC, @@ -3820,7 +3862,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "face-grin-stars" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "face grin stars", "emoticon", "eyes", "face", "grinning", "star", "star-struck", "starry-eyed" })] + [FontAwesomeSearchTerms(new[] { "face grin stars", "emoticon", "eyes", "face", "grinning", "quality", "star", "star-struck", "starry-eyed", "vip" })] [FontAwesomeCategoriesAttribute(new[] { "Emoji" })] GrinStars = 0xF587, @@ -3862,7 +3904,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "grip" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "grip", "affordance", "drag", "drop", "grab", "handle" })] + [FontAwesomeSearchTerms(new[] { "grip", "affordance", "app", "collection", "dashboard", "drag", "drop", "grab", "grid", "handle", "launcher", "square" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] GripHorizontal = 0xF58D, @@ -3925,7 +3967,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hammer" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "fix", "hammer", "recovery", "repair", "settings", "tool" })] + [FontAwesomeSearchTerms(new[] { "admin", "configuration", "equipment", "fix", "hammer", "maintenance", "modify", "recovery", "repair", "settings", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Humanitarian" })] Hammer = 0xF6E3, @@ -3953,8 +3995,8 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand-holding-droplet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand holding droplet", "carry", "covid-19", "drought", "grow", "lift", "sanitation" })] - [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands" })] + [FontAwesomeSearchTerms(new[] { "hand holding droplet", "blood", "carry", "covid-19", "drought", "grow", "lift", "sanitation" })] + [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Medical + Health" })] HandHoldingDroplet = 0xF4C1, /// @@ -3967,7 +4009,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand-holding-heart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand holding heart", "carry", "charity", "gift", "lift", "package" })] + [FontAwesomeSearchTerms(new[] { "hand holding heart", "carry", "charity", "gift", "lift", "package", "wishlist" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands" })] HandHoldingHeart = 0xF4BE, @@ -3981,7 +4023,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand-holding-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand holding dollar", "$", "carry", "dollar sign", "donation", "giving", "lift", "money", "price" })] + [FontAwesomeSearchTerms(new[] { "hand holding dollar", "$", "carry", "coupon", "dollar sign", "donate", "donation", "giving", "investment", "lift", "money", "premium", "price", "revenue", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Money" })] HandHoldingUsd = 0xF4C0, @@ -4002,7 +4044,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand", "raised hand", "backhand", "game", "halt", "palm", "raised", "raised back of hand", "roshambo", "stop" })] + [FontAwesomeSearchTerms(new[] { "hand", "raised hand", "backhand", "game", "halt", "palm", "raised", "raised back of hand", "request", "roshambo", "stop" })] [FontAwesomeCategoriesAttribute(new[] { "Hands", "Media Playback" })] HandPaper = 0xF256, @@ -4044,7 +4086,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand-point-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand point up", "finger", "hand", "hand-o-up", "index", "index pointing up", "point", "up" })] + [FontAwesomeSearchTerms(new[] { "hand point up", "finger", "hand", "hand-o-up", "index", "index pointing up", "point", "request", "up", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Hands" })] HandPointUp = 0xF0A6, @@ -4094,27 +4136,29 @@ public enum FontAwesomeIcon /// The Font Awesome "handshake" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "handshake", "agreement", "greeting", "meeting", "partnership" })] - [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Political", "Shopping" })] + [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Humanitarian", "Political", "Shopping" })] Handshake = 0xF2B5, /// - /// The Font Awesome "handshake-simple" icon unicode character. + /// The Font Awesome "handshake" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF2B5. /// - [FontAwesomeSearchTerms(new[] { "handshake simple", "agreement", "greeting", "hand", "handshake", "meeting", "partnership", "shake" })] - [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Humanitarian" })] + [FontAwesomeSearchTerms(new[] { "handshake", "agreement", "greeting", "meeting", "partnership" })] + [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Humanitarian", "Political", "Shopping" })] HandshakeSimple = 0xF4C6, /// - /// The Font Awesome "handshake-simple-slash" icon unicode character. + /// The Font Awesome "handshake-slash" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xE060. /// - [FontAwesomeSearchTerms(new[] { "handshake simple slash", "broken", "covid-19", "social distance" })] + [FontAwesomeSearchTerms(new[] { "handshake slash", "broken", "covid-19", "disabled", "social distance" })] [FontAwesomeCategoriesAttribute(new[] { "Hands" })] HandshakeSimpleSlash = 0xE05F, /// /// The Font Awesome "handshake-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "handshake slash", "broken", "covid-19", "social distance" })] + [FontAwesomeSearchTerms(new[] { "handshake slash", "broken", "covid-19", "disabled", "social distance" })] [FontAwesomeCategoriesAttribute(new[] { "Hands" })] HandshakeSlash = 0xE060, @@ -4128,7 +4172,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hands-holding-child" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hands holding child", "care", "give", "help", "hold", "protect" })] + [FontAwesomeSearchTerms(new[] { "hands holding child", "care", "give", "help", "hold", "parent", "protect" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Childhood", "Hands", "Humanitarian", "Security" })] HandsHoldingChild = 0xE4FA, @@ -4163,7 +4207,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "helmet-safety" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "helmet safety", "construction", "hardhat", "helmet", "safety" })] + [FontAwesomeSearchTerms(new[] { "helmet safety", "construction", "hardhat", "helmet", "maintenance", "safety" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Logistics" })] HardHat = 0xF807, @@ -4218,10 +4262,11 @@ public enum FontAwesomeIcon Headphones = 0xF025, /// - /// The Font Awesome "headphones-simple" icon unicode character. + /// The Font Awesome "headphones" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF025. /// - [FontAwesomeSearchTerms(new[] { "headphones simple", "audio", "listen", "music", "sound", "speaker" })] - [FontAwesomeCategoriesAttribute(new[] { "Music + Audio" })] + [FontAwesomeSearchTerms(new[] { "headphones", "audio", "earbud", "headphone", "listen", "music", "sound", "speaker" })] + [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware", "Film + Video", "Music + Audio" })] HeadphonesAlt = 0xF58F, /// @@ -4234,35 +4279,35 @@ public enum FontAwesomeIcon /// /// The Font Awesome "head-side-cough" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "head side cough", "cough", "covid-19", "germs", "lungs", "respiratory", "sick" })] + [FontAwesomeSearchTerms(new[] { "head side cough", "cough", "covid-19", "germs", "lungs", "respiratory", "sick", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] HeadSideCough = 0xE061, /// /// The Font Awesome "head-side-cough-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "head side cough slash", "cough", "covid-19", "germs", "lungs", "respiratory", "sick" })] + [FontAwesomeSearchTerms(new[] { "head side cough slash", "cough", "covid-19", "disabled", "germs", "lungs", "respiratory", "sick", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] HeadSideCoughSlash = 0xE062, /// /// The Font Awesome "head-side-mask" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "head side mask", "breath", "coronavirus", "covid-19", "filter", "flu", "infection", "pandemic", "respirator", "virus" })] + [FontAwesomeSearchTerms(new[] { "head side mask", "breath", "coronavirus", "covid-19", "filter", "flu", "infection", "pandemic", "respirator", "uer", "virus" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] HeadSideMask = 0xE063, /// /// The Font Awesome "head-side-virus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "head side virus", "cold", "coronavirus", "covid-19", "flu", "infection", "pandemic", "sick" })] + [FontAwesomeSearchTerms(new[] { "head side virus", "cold", "coronavirus", "covid-19", "flu", "infection", "pandemic", "sick", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] HeadSideVirus = 0xE064, /// /// The Font Awesome "heart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "black", "black heart", "blue", "blue heart", "brown", "brown heart", "card", "evil", "favorite", "game", "green", "green heart", "heart", "heart suit", "like", "love", "orange", "orange heart", "purple", "purple heart", "red heart", "relationship", "valentine", "white", "white heart", "wicked", "yellow", "yellow heart" })] + [FontAwesomeSearchTerms(new[] { "ace", "card", "favorite", "game", "heart", "heart suit", "like", "love", "relationship", "valentine", "wishlist" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Gaming", "Holidays", "Maps", "Medical + Health", "Shapes", "Shopping", "Social", "Sports + Fitness" })] Heart = 0xF004, @@ -4290,14 +4335,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "heart-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "heart circle check", "favorite", "heart", "love", "not affected", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "heart circle check", "enable", "favorite", "heart", "love", "not affected", "ok", "okay", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] HeartCircleCheck = 0xE4FD, /// /// The Font Awesome "heart-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "heart circle exclamation", "favorite", "heart", "love" })] + [FontAwesomeSearchTerms(new[] { "heart circle exclamation", "failed", "favorite", "heart", "love" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] HeartCircleExclamation = 0xE4FE, @@ -4318,7 +4363,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "heart-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "heart circle xmark", "favorite", "heart", "love" })] + [FontAwesomeSearchTerms(new[] { "heart circle xmark", "favorite", "heart", "love", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] HeartCircleXmark = 0xE501, @@ -4343,17 +4388,38 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian" })] HelmetUn = 0xE503, + /// + /// The Font Awesome "hexagon" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "hexagon", "horizontal black hexagon", "geometry", "honeycomb", "polygon", "shape" })] + [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] + Hexagon = 0xF312, + + /// + /// The Font Awesome "hexagon-nodes" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "hexagon nodes", "action", "ai", "artificial intelligence", "cluster", "graph", "language", "llm", "model", "network", "neuronal" })] + [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Coding" })] + HexagonNodes = 0xE699, + + /// + /// The Font Awesome "hexagon-nodes-bolt" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "hexagon nodes bolt", "llm", "action", "ai", "artificial intelligence", "cluster", "graph", "language", "llm", "model", "network", "neuronal" })] + [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Coding" })] + HexagonNodesBolt = 0xE69A, + /// /// The Font Awesome "highlighter" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "highlighter", "edit", "marker", "sharpie", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "highlighter", "edit", "marker", "modify", "sharpie", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Text Formatting" })] Highlighter = 0xF591, /// /// The Font Awesome "person-hiking" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person hiking", "autumn", "fall", "hike", "mountain", "outdoors", "summer", "walk" })] + [FontAwesomeSearchTerms(new[] { "person hiking", "autumn", "fall", "follow", "hike", "mountain", "outdoors", "summer", "uer", "walk" })] [FontAwesomeCategoriesAttribute(new[] { "Camping", "Nature", "Sports + Fitness", "Users + People" })] Hiking = 0xF6EC, @@ -4381,7 +4447,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "clock-rotate-left" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clock rotate left", "rewind", "clock", "reverse", "time", "time machine", "time travel" })] + [FontAwesomeSearchTerms(new[] { "clock rotate left", "rewind", "clock", "pending", "reverse", "time", "time machine", "time travel", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Medical + Health" })] History = 0xF1DA, @@ -4445,7 +4511,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hospital-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hospital user", "covid-19", "doctor", "network", "patient", "primary care" })] + [FontAwesomeSearchTerms(new[] { "hospital user", "covid-19", "doctor", "network", "patient", "primary care", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Medical + Health", "Users + People" })] HospitalUser = 0xF80D, @@ -4466,7 +4532,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hot-tub-person" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hot tub person", "jacuzzi", "spa" })] + [FontAwesomeSearchTerms(new[] { "hot tub person", "jacuzzi", "spa", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel", "Users + People" })] HotTub = 0xF593, @@ -4480,21 +4546,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hourglass-end" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hourglass end", "hour", "hourglass done", "minute", "sand", "stopwatch", "time", "timer" })] + [FontAwesomeSearchTerms(new[] { "hourglass end", "hour", "hourglass done", "minute", "pending", "sand", "stopwatch", "time", "timer", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] HourglassEnd = 0xF253, /// /// The Font Awesome "hourglass-half" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hourglass half", "hour", "minute", "sand", "stopwatch", "time" })] + [FontAwesomeSearchTerms(new[] { "hourglass half", "hour", "minute", "pending", "sand", "stopwatch", "time", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] HourglassHalf = 0xF252, /// /// The Font Awesome "hourglass-start" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hourglass start", "hour", "minute", "sand", "stopwatch", "time" })] + [FontAwesomeSearchTerms(new[] { "hourglass start", "hour", "minute", "sand", "stopwatch", "time", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] HourglassStart = 0xF251, @@ -4508,7 +4574,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-chimney-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house chimney user", "covid-19", "home", "isolation", "quarantine" })] + [FontAwesomeSearchTerms(new[] { "house chimney user", "covid-19", "home", "isolation", "quarantine", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Users + People" })] HouseChimneyUser = 0xE065, @@ -4522,21 +4588,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house circle check", "abode", "home", "house", "not affected", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "house circle check", "abode", "enable", "home", "house", "not affected", "ok", "okay", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] HouseCircleCheck = 0xE509, /// /// The Font Awesome "house-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house circle exclamation", "abode", "affected", "home", "house" })] + [FontAwesomeSearchTerms(new[] { "house circle exclamation", "abode", "affected", "failed", "home", "house" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] HouseCircleExclamation = 0xE50A, /// /// The Font Awesome "house-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house circle xmark", "abode", "destroy", "home", "house" })] + [FontAwesomeSearchTerms(new[] { "house circle xmark", "abode", "destroy", "home", "house", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] HouseCircleXmark = 0xE50B, @@ -4592,7 +4658,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house lock", "closed", "home", "house", "lockdown", "quarantine" })] + [FontAwesomeSearchTerms(new[] { "house lock", "closed", "home", "house", "lockdown", "padlock", "privacy", "quarantine" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Household", "Humanitarian", "Security" })] HouseLock = 0xE510, @@ -4606,21 +4672,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-medical-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house medical circle check", "clinic", "hospital", "not affected", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "house medical circle check", "clinic", "enable", "hospital", "not affected", "ok", "okay", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Medical + Health" })] HouseMedicalCircleCheck = 0xE511, /// /// The Font Awesome "house-medical-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house medical circle exclamation", "affected", "clinic", "hospital" })] + [FontAwesomeSearchTerms(new[] { "house medical circle exclamation", "affected", "clinic", "failed", "hospital" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Medical + Health" })] HouseMedicalCircleExclamation = 0xE512, /// /// The Font Awesome "house-medical-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house medical circle xmark", "clinic", "destroy", "hospital" })] + [FontAwesomeSearchTerms(new[] { "house medical circle xmark", "clinic", "destroy", "hospital", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Medical + Health" })] HouseMedicalCircleXmark = 0xE513, @@ -4634,7 +4700,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-signal" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house signal", "abode", "building", "connect", "family", "home", "residence", "smart home", "wifi" })] + [FontAwesomeSearchTerms(new[] { "house signal", "abode", "building", "connect", "family", "home", "residence", "smart home", "wifi", "www" })] [FontAwesomeCategoriesAttribute(new[] { "Connectivity", "Household", "Humanitarian" })] HouseSignal = 0xE012, @@ -4648,7 +4714,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house user", "house" })] + [FontAwesomeSearchTerms(new[] { "house user", "house", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Users + People" })] HouseUser = 0xE1B0, @@ -4690,7 +4756,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "icons" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "icons", "bolt", "emoji", "heart", "image", "music", "photo", "symbols" })] + [FontAwesomeSearchTerms(new[] { "icons", "bolt", "category", "emoji", "heart", "image", "music", "photo", "symbols" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Design", "Social", "Text Formatting" })] Icons = 0xF86D, @@ -4704,21 +4770,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "id-badge" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "id badge", "address", "contact", "identification", "license", "profile" })] + [FontAwesomeSearchTerms(new[] { "id badge", "address", "contact", "identification", "license", "profile", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Photos + Images", "Security", "Users + People" })] IdBadge = 0xF2C1, /// /// The Font Awesome "id-card" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "id card", "contact", "demographics", "document", "identification", "issued", "profile", "registration" })] + [FontAwesomeSearchTerms(new[] { "id card", "contact", "demographics", "document", "identification", "issued", "profile", "registration", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Photos + Images", "Security", "Users + People" })] IdCard = 0xF2C2, /// /// The Font Awesome "id-card-clip" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "id card clip", "contact", "demographics", "document", "identification", "issued", "profile" })] + [FontAwesomeSearchTerms(new[] { "id card clip", "contact", "demographics", "document", "identification", "issued", "profile", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Security", "Users + People" })] IdCardAlt = 0xF47F, @@ -4732,14 +4798,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "image" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "image", "album", "landscape", "photo", "picture" })] + [FontAwesomeSearchTerms(new[] { "image", "album", "img", "landscape", "photo", "picture" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Photos + Images", "Social" })] Image = 0xF03E, /// /// The Font Awesome "images" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "images", "album", "landscape", "photo", "picture" })] + [FontAwesomeSearchTerms(new[] { "images", "album", "img", "landscape", "photo", "picture" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Photos + Images", "Social" })] Images = 0xF302, @@ -4792,6 +4858,12 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Maps" })] InfoCircle = 0xF05A, + /// + /// The Font Awesome "instagramsquare" icon unicode character. + /// + [Obsolete] + InstagramSquare = 0xF955, + /// /// The Font Awesome "italic" icon unicode character. /// @@ -4921,7 +4993,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "landmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "landmark", "building", "classical", "historic", "memorable", "monument", "museum", "politics" })] + [FontAwesomeSearchTerms(new[] { "landmark", "building", "classical", "historic", "memorable", "monument", "museum", "politics", "society" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Business", "Humanitarian", "Maps", "Money" })] Landmark = 0xF66F, @@ -4956,14 +5028,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "laptop" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "computer", "cpu", "dell", "demo", "device", "laptop", "mac", "macbook", "machine", "pc", "personal" })] + [FontAwesomeSearchTerms(new[] { "computer", "cpu", "dell", "demo", "device", "fabook", "fb", "laptop", "mac", "macbook", "machine", "pc", "personal" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware", "Humanitarian" })] Laptop = 0xF109, /// /// The Font Awesome "laptop-code" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "laptop code", "computer", "cpu", "dell", "demo", "develop", "device", "mac", "macbook", "machine", "pc" })] + [FontAwesomeSearchTerms(new[] { "laptop code", "computer", "cpu", "dell", "demo", "develop", "device", "fabook", "fb", "mac", "macbook", "machine", "mysql", "pc", "sql" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Education" })] LaptopCode = 0xF5FC, @@ -5019,7 +5091,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "layer-group" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "layer group", "arrange", "develop", "layers", "map", "stack" })] + [FontAwesomeSearchTerms(new[] { "layer group", "arrange", "category", "develop", "layers", "map", "platform", "stack" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Maps" })] LayerGroup = 0xF5FD, @@ -5076,7 +5148,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "lightbulb" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "lightbulb", " comic", " electric", " idea", " innovation", " inspiration", " light", " light bulb", " bulb", "bulb", "comic", "electric", "energy", "idea", "inspiration", "mechanical" })] + [FontAwesomeSearchTerms(new[] { "lightbulb", "bulb", "bulb", "comic", "comic", "electric", "electric", "energy", "idea", "idea", "innovation", "inspiration", "inspiration", "light", "light bulb", "mechanical" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Household", "Maps", "Marketing" })] Lightbulb = 0xF0EB, @@ -5104,28 +5176,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "list" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "list", "checklist", "completed", "done", "finished", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "list", "bullet", "category", "cheatsheet", "checklist", "completed", "done", "finished", "ol", "summary", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] List = 0xF03A, /// /// The Font Awesome "rectangle-list" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "rectangle list", "checklist", "completed", "done", "finished", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "rectangle list", "cheatsheet", "checklist", "completed", "done", "finished", "ol", "summary", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ListAlt = 0xF022, /// /// The Font Awesome "list-ol" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "list ol", "checklist", "completed", "done", "finished", "numbers", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "list ol", "cheatsheet", "checklist", "completed", "done", "finished", "numbers", "ol", "summary", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ListOl = 0xF0CB, /// /// The Font Awesome "list-ul" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "list ul", "checklist", "completed", "done", "finished", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "list ul", "bullet", "cheatsheet", "checklist", "completed", "done", "finished", "ol", "summary", "survey", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ListUl = 0xF0CA, @@ -5153,21 +5225,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "location-pin-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "location pin lock", "closed", "lockdown", "map", "quarantine" })] + [FontAwesomeSearchTerms(new[] { "location pin lock", "closed", "lockdown", "map", "padlock", "privacy", "quarantine" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Maps" })] LocationPinLock = 0xE51F, /// /// The Font Awesome "lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "closed", "lock", "locked", "open", "password", "private", "protect", "security" })] + [FontAwesomeSearchTerms(new[] { "admin", "closed", "lock", "locked", "open", "padlock", "password", "privacy", "private", "protect", "security" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] Lock = 0xF023, /// /// The Font Awesome "lock-open" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "lock open", "admin", "lock", "open", "password", "private", "protect", "security", "unlock" })] + [FontAwesomeSearchTerms(new[] { "lock open", "admin", "lock", "open", "padlock", "password", "privacy", "private", "protect", "security", "unlock" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] LockOpen = 0xF3C1, @@ -5202,7 +5274,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "up-long" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "up long", "long-arrow-up", "upload" })] + [FontAwesomeSearchTerms(new[] { "up long", "long-arrow-up", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] LongArrowAltUp = 0xF30C, @@ -5251,28 +5323,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "magnifying-glass-arrow-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass arrow right", "find", "next", "search" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass arrow right", "find", "magnifier", "next", "search" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Humanitarian", "Marketing" })] MagnifyingGlassArrowRight = 0xE521, /// /// The Font Awesome "magnifying-glass-chart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass chart", " data", " graph", " intelligence", "analysis", "chart", "market" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass chart", "analysis", "chart", "data", "graph", "intelligence", "magnifier", "market", "revenue" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Humanitarian", "Marketing" })] MagnifyingGlassChart = 0xE522, /// /// The Font Awesome "envelopes-bulk" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "envelopes bulk", "archive", "envelope", "letter", "post office", "postal", "postcard", "send", "stamp", "usps" })] + [FontAwesomeSearchTerms(new[] { "envelopes bulk", "archive", "envelope", "letter", "newsletter", "offer", "post office", "postal", "postcard", "send", "stamp", "usps" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] MailBulk = 0xF674, /// /// The Font Awesome "person" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person", "man", "person standing", "stand", "standing", "woman" })] + [FontAwesomeSearchTerms(new[] { "person", "default", "man", "person standing", "stand", "standing", "uer", "woman" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Maps", "Users + People" })] Male = 0xF183, @@ -5335,7 +5407,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "marker" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "marker", "design", "edit", "sharpie", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "marker", "design", "edit", "modify", "sharpie", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design" })] Marker = 0xF5A1, @@ -5349,7 +5421,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "mars-and-venus-burst" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "mars and venus burst", "gender", "violence" })] + [FontAwesomeSearchTerms(new[] { "mars and venus burst", "gender", "uer", "violence" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] MarsAndVenusBurst = 0xE523, @@ -5412,7 +5484,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "medal" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "award", "medal", "ribbon", "sports medal", "star", "trophy" })] + [FontAwesomeSearchTerms(new[] { "award", "guarantee", "medal", "quality", "ribbon", "sports medal", "star", "trophy", "warranty" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness" })] Medal = 0xF5A2, @@ -5426,7 +5498,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "face-meh" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "face meh", "deadpan", "emoticon", "face", "meh", "neutral", "neutral face", "rating" })] + [FontAwesomeSearchTerms(new[] { "face meh", "deadpan", "default", "emoticon", "face", "meh", "neutral", "neutral face", "rating", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Emoji", "Users + People" })] Meh = 0xF11A, @@ -5482,35 +5554,35 @@ public enum FontAwesomeIcon /// /// The Font Awesome "microphone" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "microphone", "address", "audio", "information", "podcast", "public", "record", "sing", "sound", "voice" })] + [FontAwesomeSearchTerms(new[] { "microphone", "address", "audio", "information", "podcast", "public", "record", "sing", "sound", "talking", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video", "Music + Audio", "Toggle" })] Microphone = 0xF130, /// /// The Font Awesome "microphone-lines" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "microphone lines", "audio", "mic", "microphone", "music", "podcast", "record", "sing", "sound", "studio", "studio microphone", "voice" })] + [FontAwesomeSearchTerms(new[] { "microphone lines", "audio", "mic", "microphone", "music", "podcast", "record", "sing", "sound", "studio", "studio microphone", "talking", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video", "Music + Audio" })] MicrophoneAlt = 0xF3C9, /// /// The Font Awesome "microphone-lines-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "microphone lines slash", "audio", "disable", "mute", "podcast", "record", "sing", "sound", "voice" })] + [FontAwesomeSearchTerms(new[] { "microphone lines slash", "audio", "disable", "disabled", "disconnect", "disconnect", "mute", "podcast", "record", "sing", "sound", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video", "Music + Audio" })] MicrophoneAltSlash = 0xF539, /// /// The Font Awesome "microphone-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "microphone slash", "audio", "disable", "mute", "podcast", "record", "sing", "sound", "voice" })] + [FontAwesomeSearchTerms(new[] { "microphone slash", "audio", "disable", "disabled", "mute", "podcast", "record", "sing", "sound", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video", "Music + Audio", "Toggle" })] MicrophoneSlash = 0xF131, /// /// The Font Awesome "microscope" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "covid-19", "electron", "lens", "microscope", "optics", "science", "shrink", "testing", "tool" })] + [FontAwesomeSearchTerms(new[] { "covid-19", "electron", "knowledge", "lens", "microscope", "optics", "science", "shrink", "testing", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Humanitarian", "Medical + Health", "Science" })] Microscope = 0xF610, @@ -5584,73 +5656,80 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Communication", "Devices + Hardware", "Humanitarian" })] MobileScreen = 0xF3CF, + /// + /// The Font Awesome "mobile-vibrate" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "mobile vibrate", "android", "call", "cell", "cell phone", "device", "haptic", "mobile", "mobile phone", "notification", "number", "phone", "screen", "telephone", "text" })] + [FontAwesomeCategoriesAttribute(new[] { "Communication", "Devices + Hardware" })] + MobileVibrate = 0xE816, + /// /// The Font Awesome "money-bill" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill", "buy", "cash", "checkout", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money bill", "buy", "cash", "checkout", "coupon", "investment", "money", "payment", "premium", "price", "purchase", "revenue", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Money" })] MoneyBill = 0xF0D6, /// /// The Font Awesome "money-bill-1" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill 1", "buy", "cash", "checkout", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money bill 1", "buy", "cash", "checkout", "money", "payment", "premium", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Money" })] MoneyBillAlt = 0xF3D1, /// /// The Font Awesome "money-bills" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bills", "atm", "cash", "money", "moolah" })] + [FontAwesomeSearchTerms(new[] { "money bills", "atm", "cash", "investment", "money", "moolah", "premium", "revenue", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] MoneyBills = 0xE1F3, /// /// The Font Awesome "money-bill-transfer" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill transfer", "bank", "conversion", "deposit", "money", "transfer", "withdrawal" })] + [FontAwesomeSearchTerms(new[] { "money bill transfer", "bank", "conversion", "deposit", "investment", "money", "salary", "transfer", "withdrawal" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] MoneyBillTransfer = 0xE528, /// /// The Font Awesome "money-bill-trend-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill trend up", "bank", "bonds", "inflation", "market", "stocks", "trade" })] + [FontAwesomeSearchTerms(new[] { "money bill trend up", "bank", "bonds", "inflation", "investment", "market", "revenue", "salary", "stocks", "trade" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] MoneyBillTrendUp = 0xE529, /// /// The Font Awesome "money-bill-wave" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill wave", "buy", "cash", "checkout", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money bill wave", "buy", "cash", "checkout", "money", "payment", "premium", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Money" })] MoneyBillWave = 0xF53A, /// /// The Font Awesome "money-bill-1-wave" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill 1 wave", "buy", "cash", "checkout", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money bill 1 wave", "buy", "cash", "checkout", "money", "payment", "premium", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Money" })] MoneyBillWaveAlt = 0xF53B, /// /// The Font Awesome "money-bill-wheat" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill wheat", "agribusiness", "agriculture", "farming", "food", "livelihood", "subsidy" })] + [FontAwesomeSearchTerms(new[] { "money bill wheat", "agribusiness", "agriculture", "farming", "food", "investment", "livelihood", "subsidy" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] MoneyBillWheat = 0xE52A, /// /// The Font Awesome "money-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money check", "bank check", "buy", "checkout", "cheque", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money check", "bank check", "buy", "checkout", "cheque", "money", "payment", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Money", "Shopping" })] MoneyCheck = 0xF53C, /// /// The Font Awesome "money-check-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money check dollar", "bank check", "buy", "checkout", "cheque", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money check dollar", "bank check", "buy", "checkout", "cheque", "money", "payment", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Money", "Shopping" })] MoneyCheckAlt = 0xF53D, @@ -5783,14 +5862,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "newspaper" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "article", "editorial", "headline", "journal", "journalism", "news", "newspaper", "paper", "press" })] + [FontAwesomeSearchTerms(new[] { "article", "editorial", "headline", "journal", "journalism", "news", "newsletter", "newspaper", "paper", "press" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Writing" })] Newspaper = 0xF1EA, + /// + /// The Font Awesome "non-binary" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "non binary", "female", "gender", "male", "nb", "queer" })] + [FontAwesomeCategoriesAttribute(new[] { "Genders" })] + NonBinary = 0xE807, + /// /// The Font Awesome "notdef" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "notdef", "close", "missing" })] + [FontAwesomeSearchTerms(new[] { "notdef", "404", "close", "missing", "not found" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Writing" })] Notdef = 0xE1FE, @@ -5822,6 +5908,13 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Design" })] ObjectUngroup = 0xF248, + /// + /// The Font Awesome "octagon" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "octagon", "octagonal", "shape", "sign", "stop", "stop sign" })] + [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] + Octagon = 0xF306, + /// /// The Font Awesome "oil-can" icon unicode character. /// @@ -5867,14 +5960,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "paintbrush" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "acrylic", "art", "brush", "color", "fill", "paint", "paintbrush", "painting", "pigment", "watercolor" })] + [FontAwesomeSearchTerms(new[] { "acrylic", "art", "brush", "color", "fill", "modify", "paint", "paintbrush", "painting", "pigment", "watercolor" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Editing" })] PaintBrush = 0xF1FC, /// /// The Font Awesome "paint-roller" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "paint roller", "acrylic", "art", "brush", "color", "fill", "paint", "pigment", "watercolor" })] + [FontAwesomeSearchTerms(new[] { "paint roller", "acrylic", "art", "brush", "color", "fill", "maintenance", "paint", "pigment", "watercolor" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Design" })] PaintRoller = 0xF5AA, @@ -5895,7 +5988,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "panorama" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "panorama", "image", "landscape", "photo", "wide" })] + [FontAwesomeSearchTerms(new[] { "panorama", "image", "img", "landscape", "photo", "wide" })] [FontAwesomeCategoriesAttribute(new[] { "Photos + Images" })] Panorama = 0xE209, @@ -5986,98 +6079,105 @@ public enum FontAwesomeIcon /// /// The Font Awesome "pen" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ballpoint", "design", "edit", "pen", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "ballpoint", "design", "edit", "modify", "pen", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing", "Writing" })] Pen = 0xF304, /// /// The Font Awesome "pen-clip" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen clip", "design", "edit", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "pen clip", "design", "edit", "modify", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing", "Writing" })] PenAlt = 0xF305, /// /// The Font Awesome "pencil" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "lower left pencil", "design", "draw", "edit", "lead", "pencil", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "lower left pencil", "design", "draw", "edit", "lead", "maintenance", "modify", "pencil", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Construction", "Design", "Editing", "Writing" })] PencilAlt = 0xF303, /// /// The Font Awesome "pen-ruler" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen ruler", "design", "draft", "draw", "pencil" })] + [FontAwesomeSearchTerms(new[] { "pen ruler", "design", "draft", "draw", "maintenance", "modify", "pencil" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Design", "Editing" })] PencilRuler = 0xF5AE, /// /// The Font Awesome "pen-fancy" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen fancy", "black nib", "design", "edit", "fountain", "fountain pen", "nib", "pen", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "pen fancy", "black nib", "design", "edit", "fountain", "fountain pen", "modify", "nib", "pen", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing" })] PenFancy = 0xF5AC, /// /// The Font Awesome "pen-nib" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen nib", "design", "edit", "fountain pen", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "pen nib", "design", "edit", "fountain pen", "modify", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing" })] PenNib = 0xF5AD, /// /// The Font Awesome "square-pen" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square pen", "edit", "pencil-square", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "square pen", "edit", "modify", "pencil-square", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Editing", "Writing" })] PenSquare = 0xF14B, + /// + /// The Font Awesome "pentagon" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "5", "five", "pentagon", "shape" })] + [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] + Pentagon = 0xE790, + /// /// The Font Awesome "people-arrows" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people arrows", "distance", "isolation", "separate", "social distancing", "users-people" })] + [FontAwesomeSearchTerms(new[] { "people arrows", "conversation", "discussion", "distance", "insert", "isolation", "separate", "social distancing", "talk", "talking", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PeopleArrows = 0xE068, /// /// The Font Awesome "people-carry-box" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people carry box", "users-people" })] + [FontAwesomeSearchTerms(new[] { "people carry box", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Moving", "Users + People" })] PeopleCarry = 0xF4CE, /// /// The Font Awesome "people-group" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people group", "family", "group", "team" })] + [FontAwesomeSearchTerms(new[] { "people group", "crowd", "family", "group", "team", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Marketing", "Users + People" })] PeopleGroup = 0xE533, /// /// The Font Awesome "people-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people line", "group", "need" })] + [FontAwesomeSearchTerms(new[] { "people line", "crowd", "group", "need", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PeopleLine = 0xE534, /// /// The Font Awesome "people-pulling" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people pulling", "forced return", "yanking" })] + [FontAwesomeSearchTerms(new[] { "people pulling", "forced return", "together", "uer", "yanking" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PeoplePulling = 0xE535, /// /// The Font Awesome "people-robbery" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people robbery", "criminal", "hands up", "looting", "robbery", "steal" })] + [FontAwesomeSearchTerms(new[] { "people robbery", "criminal", "hands up", "looting", "robbery", "steal", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PeopleRobbery = 0xE536, /// /// The Font Awesome "people-roof" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people roof", "family", "group", "manage", "people", "safe", "shelter" })] + [FontAwesomeSearchTerms(new[] { "people roof", "crowd", "family", "group", "manage", "people", "safe", "shelter", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Camping", "Household", "Humanitarian", "Users + People" })] PeopleRoof = 0xE537, @@ -6107,105 +6207,105 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-arrow-down-to-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person arrow down to line", "ground", "indigenous", "native" })] + [FontAwesomeSearchTerms(new[] { "person arrow down to line", "ground", "indigenous", "insert", "native", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonArrowDownToLine = 0xE538, /// /// The Font Awesome "person-arrow-up-from-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person arrow up from line", "population", "rise" })] + [FontAwesomeSearchTerms(new[] { "person arrow up from line", "population", "rise", "uer", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonArrowUpFromLine = 0xE539, /// /// The Font Awesome "person-booth" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person booth", "changing room", "curtain", "vote", "voting" })] + [FontAwesomeSearchTerms(new[] { "person booth", "changing room", "curtain", "uer", "vote", "voting" })] [FontAwesomeCategoriesAttribute(new[] { "Political", "Shopping", "Users + People" })] PersonBooth = 0xF756, /// /// The Font Awesome "person-breastfeeding" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person breastfeeding", "baby", "child", "infant", "mother", "nutrition", "sustenance" })] + [FontAwesomeSearchTerms(new[] { "person breastfeeding", "baby", "child", "infant", "mother", "nutrition", "parent", "sustenance", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Humanitarian", "Medical + Health", "Users + People" })] PersonBreastfeeding = 0xE53A, /// /// The Font Awesome "person-burst" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person burst", "abuse", "accident", "crash", "explode", "violence" })] + [FontAwesomeSearchTerms(new[] { "person burst", "abuse", "accident", "crash", "explode", "uer", "violence" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonBurst = 0xE53B, /// /// The Font Awesome "person-cane" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person cane", "aging", "cane", "elderly", "old", "staff" })] + [FontAwesomeSearchTerms(new[] { "person cane", "aging", "cane", "elderly", "old", "staff", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Humanitarian", "Medical + Health", "Users + People" })] PersonCane = 0xE53C, /// /// The Font Awesome "person-chalkboard" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person chalkboard", "blackboard", "instructor", "keynote", "lesson", "presentation", "teacher" })] + [FontAwesomeSearchTerms(new[] { "person chalkboard", "blackboard", "instructor", "keynote", "lesson", "presentation", "teacher", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Education", "Humanitarian", "Users + People" })] PersonChalkboard = 0xE53D, /// /// The Font Awesome "person-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle check", "approved", "not affected", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "person circle check", "approved", "enable", "not affected", "ok", "okay", "uer", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleCheck = 0xE53E, /// /// The Font Awesome "person-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle exclamation", "affected", "alert", "lost", "missing" })] + [FontAwesomeSearchTerms(new[] { "person circle exclamation", "affected", "alert", "failed", "lost", "missing", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleExclamation = 0xE53F, /// /// The Font Awesome "person-circle-minus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle minus", "delete", "remove" })] + [FontAwesomeSearchTerms(new[] { "person circle minus", "delete", "remove", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleMinus = 0xE540, /// /// The Font Awesome "person-circle-plus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle plus", "add", "found" })] + [FontAwesomeSearchTerms(new[] { "person circle plus", "add", "follow", "found", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCirclePlus = 0xE541, /// /// The Font Awesome "person-circle-question" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle question", "lost", "missing" })] + [FontAwesomeSearchTerms(new[] { "person circle question", "faq", "lost", "missing", "request", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleQuestion = 0xE542, /// /// The Font Awesome "person-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle xmark", "dead", "removed" })] + [FontAwesomeSearchTerms(new[] { "person circle xmark", "dead", "removed", "uer", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleXmark = 0xE543, /// /// The Font Awesome "person-digging" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person digging", "bury", "construction", "debris", "dig", "men at work" })] + [FontAwesomeSearchTerms(new[] { "person digging", "bury", "construction", "debris", "dig", "maintenance", "men at work", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Humanitarian", "Users + People" })] PersonDigging = 0xF85E, /// /// The Font Awesome "person-dress-burst" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person dress burst", "abuse", "accident", "crash", "explode", "violence" })] + [FontAwesomeSearchTerms(new[] { "person dress burst", "abuse", "accident", "crash", "explode", "uer", "violence" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonDressBurst = 0xE544, @@ -6219,112 +6319,112 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-falling" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person falling", "accident", "fall", "trip" })] + [FontAwesomeSearchTerms(new[] { "person falling", "accident", "fall", "trip", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonFalling = 0xE546, /// /// The Font Awesome "person-falling-burst" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person falling burst", "accident", "crash", "death", "fall", "homicide", "murder" })] + [FontAwesomeSearchTerms(new[] { "person falling burst", "accident", "crash", "death", "fall", "homicide", "murder", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonFallingBurst = 0xE547, /// /// The Font Awesome "person-half-dress" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person half dress", "gender", "man", "restroom", "transgender", "woman" })] + [FontAwesomeSearchTerms(new[] { "person half dress", "gender", "man", "restroom", "transgender", "uer", "woman" })] [FontAwesomeCategoriesAttribute(new[] { "Genders", "Humanitarian", "Medical + Health", "Users + People" })] PersonHalfDress = 0xE548, /// /// The Font Awesome "person-harassing" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person harassing", "abuse", "scream", "shame", "shout", "yell" })] + [FontAwesomeSearchTerms(new[] { "person harassing", "abuse", "scream", "shame", "shout", "uer", "yell" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonHarassing = 0xE549, /// /// The Font Awesome "person-military-pointing" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person military pointing", "army", "customs", "guard" })] + [FontAwesomeSearchTerms(new[] { "person military pointing", "army", "customs", "guard", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonMilitaryPointing = 0xE54A, /// /// The Font Awesome "person-military-rifle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person military rifle", "armed forces", "army", "military", "rifle", "war" })] + [FontAwesomeSearchTerms(new[] { "person military rifle", "armed forces", "army", "military", "rifle", "uer", "war" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonMilitaryRifle = 0xE54B, /// /// The Font Awesome "person-military-to-person" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person military to person", "civilian", "coordination", "military" })] + [FontAwesomeSearchTerms(new[] { "person military to person", "civilian", "coordination", "military", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonMilitaryToPerson = 0xE54C, /// /// The Font Awesome "person-pregnant" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person pregnant", "baby", "birth", "child", "pregnant", "pregnant woman", "woman" })] + [FontAwesomeSearchTerms(new[] { "person pregnant", "baby", "birth", "child", "parent", "pregnant", "pregnant woman", "uer", "woman" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonPregnant = 0xE31E, /// /// The Font Awesome "person-rays" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person rays", "affected", "focus", "shine" })] + [FontAwesomeSearchTerms(new[] { "person rays", "affected", "focus", "shine", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Marketing", "Users + People" })] PersonRays = 0xE54D, /// /// The Font Awesome "person-rifle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person rifle", "army", "combatant", "gun", "military", "rifle", "war" })] + [FontAwesomeSearchTerms(new[] { "person rifle", "army", "combatant", "gun", "military", "rifle", "uer", "war" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Security", "Users + People" })] PersonRifle = 0xE54E, /// /// The Font Awesome "person-shelter" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person shelter", "house", "inside", "roof", "safe", "safety", "shelter" })] + [FontAwesomeSearchTerms(new[] { "person shelter", "house", "inside", "roof", "safe", "safety", "shelter", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Camping", "Humanitarian", "Security", "Users + People" })] PersonShelter = 0xE54F, /// /// The Font Awesome "person-through-window" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person through window", "door", "exit", "forced entry", "leave", "robbery", "steal", "window" })] + [FontAwesomeSearchTerms(new[] { "person through window", "door", "exit", "forced entry", "leave", "robbery", "steal", "uer", "window" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonThroughWindow = 0xE5A9, /// /// The Font Awesome "person-walking-arrow-loop-left" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking arrow loop left", "population return", "return" })] + [FontAwesomeSearchTerms(new[] { "person walking arrow loop left", "follow", "population return", "return", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Users + People" })] PersonWalkingArrowLoopLeft = 0xE551, /// /// The Font Awesome "person-walking-arrow-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking arrow right", "exit", "internally displaced", "leave", "refugee" })] + [FontAwesomeSearchTerms(new[] { "person walking arrow right", "exit", "follow", "internally displaced", "leave", "refugee", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Users + People" })] PersonWalkingArrowRight = 0xE552, /// /// The Font Awesome "person-walking-dashed-line-arrow-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking dashed line arrow right", "exit", "refugee" })] + [FontAwesomeSearchTerms(new[] { "person walking dashed line arrow right", "exit", "follow", "refugee", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Users + People" })] PersonWalkingDashedLineArrowRight = 0xE553, /// /// The Font Awesome "person-walking-luggage" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking luggage", "bag", "baggage", "briefcase", "carry-on", "deployment", "rolling" })] + [FontAwesomeSearchTerms(new[] { "person walking luggage", "bag", "baggage", "briefcase", "carry-on", "deployment", "follow", "rolling", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Travel + Hotel", "Users + People" })] PersonWalkingLuggage = 0xE554, @@ -6345,7 +6445,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "phone" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "left hand telephone receiver", "call", "earphone", "number", "phone", "receiver", "support", "telephone", "telephone receiver", "voice" })] + [FontAwesomeSearchTerms(new[] { "left hand telephone receiver", "call", "earphone", "number", "phone", "receiver", "support", "talking", "telephone", "telephone receiver", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Maps" })] Phone = 0xF095, @@ -6359,7 +6459,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "phone-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "phone slash", "call", "cancel", "earphone", "mute", "number", "support", "telephone", "voice" })] + [FontAwesomeSearchTerms(new[] { "phone slash", "call", "cancel", "disabled", "disconnect", "earphone", "mute", "number", "support", "telephone", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication" })] PhoneSlash = 0xF3DD, @@ -6380,7 +6480,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "phone-volume" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "phone volume", "call", "earphone", "number", "sound", "support", "telephone", "voice", "volume-control-phone" })] + [FontAwesomeSearchTerms(new[] { "phone volume", "call", "earphone", "number", "ring", "ringing", "sound", "support", "talking", "telephone", "voice", "volume-control-phone" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Business", "Communication", "Maps", "Media Playback" })] PhoneVolume = 0xF2A0, @@ -6394,7 +6494,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "piggy-bank" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "piggy bank", "bank", "save", "savings" })] + [FontAwesomeSearchTerms(new[] { "piggy bank", "bank", "salary", "save", "savings" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Money", "Political" })] PiggyBank = 0xF4D3, @@ -6430,27 +6530,27 @@ public enum FontAwesomeIcon /// The Font Awesome "plane-arrival" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "plane arrival", "aeroplane", "airplane", "airplane arrival", "airport", "arrivals", "arriving", "destination", "fly", "land", "landing", "location", "mode", "travel", "trip" })] - [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel" })] + [FontAwesomeCategoriesAttribute(new[] { "Transportation", "Travel + Hotel" })] PlaneArrival = 0xF5AF, /// /// The Font Awesome "plane-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane circle check", "airplane", "airport", "flight", "fly", "not affected", "ok", "okay", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane circle check", "airplane", "airport", "enable", "flight", "fly", "not affected", "ok", "okay", "travel", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Travel + Hotel" })] PlaneCircleCheck = 0xE555, /// /// The Font Awesome "plane-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane circle exclamation", "affected", "airplane", "airport", "flight", "fly", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane circle exclamation", "affected", "airplane", "airport", "failed", "flight", "fly", "travel" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Travel + Hotel" })] PlaneCircleExclamation = 0xE556, /// /// The Font Awesome "plane-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane circle xmark", "airplane", "airport", "destroy", "flight", "fly", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane circle xmark", "airplane", "airport", "destroy", "flight", "fly", "travel", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Travel + Hotel" })] PlaneCircleXmark = 0xE557, @@ -6464,14 +6564,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "plane-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane lock", "airplane", "airport", "closed", "flight", "fly", "lockdown", "quarantine", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane lock", "airplane", "airport", "closed", "flight", "fly", "lockdown", "padlock", "privacy", "quarantine", "travel" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Travel + Hotel" })] PlaneLock = 0xE558, /// /// The Font Awesome "plane-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane slash", "airplane mode", "airport", "canceled", "covid-19", "delayed", "grounded", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane slash", "airplane mode", "airport", "canceled", "covid-19", "delayed", "disabled", "grounded", "travel" })] [FontAwesomeCategoriesAttribute(new[] { "Transportation", "Travel + Hotel" })] PlaneSlash = 0xE069, @@ -6527,21 +6627,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "plug-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plug circle check", "electric", "electricity", "not affected", "ok", "okay", "plug", "power" })] + [FontAwesomeSearchTerms(new[] { "plug circle check", "electric", "electricity", "enable", "not affected", "ok", "okay", "plug", "power", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Humanitarian" })] PlugCircleCheck = 0xE55C, /// /// The Font Awesome "plug-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plug circle exclamation", "affected", "electric", "electricity", "plug", "power" })] + [FontAwesomeSearchTerms(new[] { "plug circle exclamation", "affected", "electric", "electricity", "failed", "plug", "power" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Humanitarian" })] PlugCircleExclamation = 0xE55D, /// /// The Font Awesome "plug-circle-minus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plug circle minus", "electric", "electricity", "plug", "power" })] + [FontAwesomeSearchTerms(new[] { "plug circle minus", "disconnect", "electric", "electricity", "plug", "power" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Humanitarian" })] PlugCircleMinus = 0xE55E, @@ -6555,7 +6655,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "plug-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plug circle xmark", "destroy", "electric", "electricity", "outage", "plug", "power" })] + [FontAwesomeSearchTerms(new[] { "plug circle xmark", "destroy", "disconnect", "electric", "electricity", "outage", "plug", "power", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Humanitarian" })] PlugCircleXmark = 0xE560, @@ -6563,7 +6663,7 @@ public enum FontAwesomeIcon /// The Font Awesome "plus" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x2B. ///
- [FontAwesomeSearchTerms(new[] { "+", "plus sign", "add", "create", "expand", "math", "new", "plus", "positive", "shape", "sign" })] + [FontAwesomeSearchTerms(new[] { "+", "plus sign", "add", "create", "expand", "follow", "math", "modify", "new", "plus", "positive", "shape", "sign" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Maps", "Mathematics", "Medical + Health", "Punctuation + Symbols" })] Plus = 0xF067, @@ -6598,21 +6698,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-poll-vertical" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square poll vertical", "chart", "graph", "results", "survey", "trend", "vote", "voting" })] + [FontAwesomeSearchTerms(new[] { "square poll vertical", "chart", "graph", "results", "revenue", "statistics", "survey", "trend", "vote", "voting" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Marketing", "Social" })] Poll = 0xF681, /// /// The Font Awesome "square-poll-horizontal" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square poll horizontal", "chart", "graph", "results", "survey", "trend", "vote", "voting" })] + [FontAwesomeSearchTerms(new[] { "square poll horizontal", "chart", "graph", "results", "statistics", "survey", "trend", "vote", "voting" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Marketing", "Social" })] PollH = 0xF682, /// /// The Font Awesome "poo" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "crap", "dung", "face", "monster", "pile of poo", "poo", "poop", "shit", "smile", "turd" })] + [FontAwesomeSearchTerms(new[] { "crap", "dung", "face", "monster", "pile of poo", "poo", "poop", "shit", "smile", "turd", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Users + People" })] Poo = 0xF2FE, @@ -6633,7 +6733,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "image-portrait" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "image portrait", "id", "image", "photo", "picture", "selfie" })] + [FontAwesomeSearchTerms(new[] { "image portrait", "id", "image", "img", "photo", "picture", "selfie", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Photos + Images", "Users + People" })] Portrait = 0xF3E0, @@ -6654,7 +6754,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-praying" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person praying", "kneel", "place of worship", "religion", "thank", "worship" })] + [FontAwesomeSearchTerms(new[] { "person praying", "kneel", "place of worship", "religion", "thank", "uer", "worship" })] [FontAwesomeCategoriesAttribute(new[] { "Religion", "Users + People" })] Pray = 0xF683, @@ -6703,7 +6803,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "diagram-project" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "diagram project", "chart", "graph", "network", "pert" })] + [FontAwesomeSearchTerms(new[] { "diagram project", "chart", "graph", "network", "pert", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Coding" })] ProjectDiagram = 0xF542, @@ -6731,22 +6831,22 @@ public enum FontAwesomeIcon /// /// The Font Awesome "qrcode" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "qrcode", "barcode", "info", "information", "scan" })] - [FontAwesomeCategoriesAttribute(new[] { "Coding" })] + [FontAwesomeSearchTerms(new[] { "qrcode", "barcode", "info", "information", "qr", "qr-code", "scan" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Shopping" })] Qrcode = 0xF029, /// /// The Font Awesome "question" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x3F. /// - [FontAwesomeSearchTerms(new[] { "?", "question mark", "help", "information", "mark", "outlined", "punctuation", "question", "red question mark", "support", "unknown", "white question mark" })] + [FontAwesomeSearchTerms(new[] { "?", "question mark", "faq", "help", "information", "mark", "outlined", "punctuation", "question", "red question mark", "request", "support", "unknown", "white question mark" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Alert", "Punctuation + Symbols" })] Question = 0xF128, /// /// The Font Awesome "circle-question" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle question", "help", "information", "support", "unknown" })] + [FontAwesomeSearchTerms(new[] { "circle question", "faq", "help", "information", "support", "unknown" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Punctuation + Symbols" })] QuestionCircle = 0xF059, @@ -6816,14 +6916,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "ranking-star" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ranking star", "chart", "first place", "podium", "rank", "win" })] + [FontAwesomeSearchTerms(new[] { "ranking star", "chart", "first place", "podium", "quality", "rank", "revenue", "win" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Marketing", "Sports + Fitness" })] RankingStar = 0xE561, /// /// The Font Awesome "receipt" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "accounting", "bookkeeping", "check", "evidence", "invoice", "money", "pay", "proof", "receipt", "table" })] + [FontAwesomeSearchTerms(new[] { "accounting", "bookkeeping", "check", "coupon", "evidence", "invoice", "money", "pay", "proof", "receipt", "table" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Money", "Shopping" })] Receipt = 0xF543, @@ -6844,15 +6944,15 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-rotate-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow rotate right", "clockwise open circle arrow", "forward", "refresh", "reload", "repeat" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] + [FontAwesomeSearchTerms(new[] { "arrow rotate right", "clockwise open circle arrow", "forward", "refresh", "reload", "renew", "repeat", "retry" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback", "Spinners" })] Redo = 0xF01E, /// /// The Font Awesome "rotate-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "rotate right", "forward", "refresh", "reload", "repeat" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] + [FontAwesomeSearchTerms(new[] { "rotate right", "forward", "refresh", "reload", "renew", "repeat", "retry" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback", "Spinners" })] RedoAlt = 0xF2F9, /// @@ -6865,14 +6965,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "text-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "text slash", "cancel", "font", "format", "remove", "style", "text" })] + [FontAwesomeSearchTerms(new[] { "text slash", "cancel", "disabled", "font", "format", "remove", "style", "text" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] RemoveFormat = 0xF87D, /// /// The Font Awesome "repeat" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow", "clockwise", "flip", "reload", "repeat", "repeat button", "rewind", "switch" })] + [FontAwesomeSearchTerms(new[] { "arrow", "clockwise", "flip", "reload", "renew", "repeat", "repeat button", "retry", "rewind", "switch" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] Repeat = 0xF363, @@ -6900,14 +7000,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "restroom" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "restroom", "bathroom", "toilet", "water closet", "wc" })] + [FontAwesomeSearchTerms(new[] { "restroom", "bathroom", "toilet", "uer", "water closet", "wc" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Users + People" })] Restroom = 0xF7BD, /// /// The Font Awesome "retweet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "retweet", "refresh", "reload", "share", "swap" })] + [FontAwesomeSearchTerms(new[] { "retweet", "refresh", "reload", "renew", "retry", "share", "swap" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Social" })] Retweet = 0xF079, @@ -6921,7 +7021,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "ring" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ring", "dungeons & dragons", "gollum", "band", "binding", "d&d", "dnd", "engagement", "fantasy", "gold", "jewelry", "marriage", "precious" })] + [FontAwesomeSearchTerms(new[] { "ring", "dungeons & dragons", "gollum", "band", "binding", "d&d", "dnd", "engagement", "fantasy", "gold", "jewelry", "marriage", "precious", "premium" })] [FontAwesomeCategoriesAttribute(new[] { "Gaming", "Spinners" })] Ring = 0xF70B, @@ -6949,28 +7049,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "road-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "road circle check", "freeway", "highway", "not affected", "ok", "okay", "pavement", "road" })] + [FontAwesomeSearchTerms(new[] { "road circle check", "enable", "freeway", "highway", "not affected", "ok", "okay", "pavement", "road", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] RoadCircleCheck = 0xE564, /// /// The Font Awesome "road-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "road circle exclamation", "affected", "freeway", "highway", "pavement", "road" })] + [FontAwesomeSearchTerms(new[] { "road circle exclamation", "affected", "failed", "freeway", "highway", "pavement", "road" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] RoadCircleExclamation = 0xE565, /// /// The Font Awesome "road-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "road circle xmark", "destroy", "freeway", "highway", "pavement", "road" })] + [FontAwesomeSearchTerms(new[] { "road circle xmark", "destroy", "freeway", "highway", "pavement", "road", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] RoadCircleXmark = 0xE566, /// /// The Font Awesome "road-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "road lock", "closed", "freeway", "highway", "lockdown", "pavement", "quarantine", "road" })] + [FontAwesomeSearchTerms(new[] { "road lock", "closed", "freeway", "highway", "lockdown", "padlock", "pavement", "privacy", "quarantine", "road" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] RoadLock = 0xE567, @@ -7061,7 +7161,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-running" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person running", "exit", "flee", "marathon", "person running", "race", "running" })] + [FontAwesomeSearchTerms(new[] { "person running", "exit", "flee", "follow", "marathon", "person running", "race", "running", "uer", "workout" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] Running = 0xF70C, @@ -7082,14 +7182,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "sack-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sack dollar", "bag", "burlap", "cash", "dollar", "money", "money bag", "moneybag", "robber", "santa", "usd" })] + [FontAwesomeSearchTerms(new[] { "sack dollar", "bag", "burlap", "cash", "dollar", "investment", "money", "money bag", "moneybag", "premium", "robber", "salary", "santa", "usd" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] SackDollar = 0xF81D, /// /// The Font Awesome "sack-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sack xmark", "bag", "burlap", "rations" })] + [FontAwesomeSearchTerms(new[] { "sack xmark", "bag", "burlap", "coupon", "rations", "salary", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] SackXmark = 0xE56A, @@ -7145,21 +7245,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "school-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "school circle check", "not affected", "ok", "okay", "schoolhouse" })] + [FontAwesomeSearchTerms(new[] { "school circle check", "enable", "not affected", "ok", "okay", "schoolhouse", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Education", "Humanitarian" })] SchoolCircleCheck = 0xE56B, /// /// The Font Awesome "school-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "school circle exclamation", "affected", "schoolhouse" })] + [FontAwesomeSearchTerms(new[] { "school circle exclamation", "affected", "failed", "schoolhouse" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Education", "Humanitarian" })] SchoolCircleExclamation = 0xE56C, /// /// The Font Awesome "school-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "school circle xmark", "destroy", "schoolhouse" })] + [FontAwesomeSearchTerms(new[] { "school circle xmark", "destroy", "schoolhouse", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Education", "Humanitarian" })] SchoolCircleXmark = 0xE56D, @@ -7173,63 +7273,63 @@ public enum FontAwesomeIcon /// /// The Font Awesome "school-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "school lock", "closed", "lockdown", "quarantine", "schoolhouse" })] + [FontAwesomeSearchTerms(new[] { "school lock", "closed", "lockdown", "padlock", "privacy", "quarantine", "schoolhouse" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Education", "Humanitarian" })] SchoolLock = 0xE56F, /// /// The Font Awesome "screwdriver" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "fix", "mechanic", "repair", "screw", "screwdriver", "settings", "tool" })] + [FontAwesomeSearchTerms(new[] { "admin", "configuration", "equipment", "fix", "maintenance", "mechanic", "modify", "repair", "screw", "screwdriver", "settings", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Construction" })] Screwdriver = 0xF54A, /// /// The Font Awesome "scroll" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "dungeons & dragons", "announcement", "d&d", "dnd", "fantasy", "paper", "script", "scroll" })] + [FontAwesomeSearchTerms(new[] { "dungeons & dragons", "announcement", "d&d", "dnd", "fantasy", "paper", "scholar", "script", "scroll" })] [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] Scroll = 0xF70E, /// /// The Font Awesome "sd-card" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sd card", "image", "memory", "photo", "save" })] + [FontAwesomeSearchTerms(new[] { "sd card", "image", "img", "memory", "photo", "save" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] SdCard = 0xF7C2, /// /// The Font Awesome "magnifying-glass" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass", "bigger", "enlarge", "find", "glass", "magnify", "magnifying", "magnifying glass tilted left", "preview", "search", "tool", "zoom" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass", "bigger", "enlarge", "equipment", "find", "glass", "inspection", "magnifier", "magnify", "magnifying", "magnifying glass tilted left", "preview", "search", "tool", "zoom" })] [FontAwesomeCategoriesAttribute(new[] { "Maps" })] Search = 0xF002, /// /// The Font Awesome "magnifying-glass-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass dollar", "bigger", "enlarge", "find", "magnify", "money", "preview", "zoom" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass dollar", "bigger", "enlarge", "find", "magnifier", "magnify", "money", "preview", "zoom" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] SearchDollar = 0xF688, /// /// The Font Awesome "magnifying-glass-location" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass location", "bigger", "enlarge", "find", "magnify", "preview", "zoom" })] - [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass location", "bigger", "enlarge", "find", "magnifier", "magnify", "preview", "zoom" })] + [FontAwesomeCategoriesAttribute(new[] { "Maps", "Marketing" })] SearchLocation = 0xF689, /// /// The Font Awesome "magnifying-glass-minus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass minus", "minify", "negative", "smaller", "zoom", "zoom out" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass minus", "magnifier", "minify", "negative", "smaller", "zoom", "zoom out" })] [FontAwesomeCategoriesAttribute(new[] { "Maps" })] SearchMinus = 0xF010, /// /// The Font Awesome "magnifying-glass-plus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass plus", "bigger", "enlarge", "magnify", "positive", "zoom", "zoom in" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass plus", "bigger", "enlarge", "magnifier", "magnify", "positive", "zoom", "zoom in" })] [FontAwesomeCategoriesAttribute(new[] { "Maps" })] SearchPlus = 0xF00E, @@ -7243,14 +7343,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "seedling" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "environment", "flora", "grow", "plant", "sapling", "seedling", "vegan", "young" })] + [FontAwesomeSearchTerms(new[] { "environment", "flora", "grow", "investment", "plant", "sapling", "seedling", "vegan", "young" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Energy", "Food + Beverage", "Fruits + Vegetables", "Humanitarian", "Nature", "Science" })] Seedling = 0xF4D8, + /// + /// The Font Awesome "septagon" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "septagon", "7", "heptagon", "seven", "shape" })] + [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] + Septagon = 0xE820, + /// /// The Font Awesome "server" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "server", "computer", "cpu", "database", "hardware", "network" })] + [FontAwesomeSearchTerms(new[] { "server", "computer", "cpu", "database", "hardware", "mysql", "network", "sql" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] Server = 0xF233, @@ -7313,7 +7420,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "shield-halved" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "shield halved", "achievement", "armor", "award", "block", "cleric", "defend", "defense", "holy", "paladin", "security", "shield", "weapon", "winner" })] + [FontAwesomeSearchTerms(new[] { "shield halved", "achievement", "armor", "award", "block", "cleric", "defend", "defense", "holy", "paladin", "privacy", "security", "shield", "weapon", "winner" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Gaming", "Security" })] ShieldAlt = 0xF3ED, @@ -7334,7 +7441,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "shield-heart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "shield heart", "love", "protect", "safe", "safety", "shield" })] + [FontAwesomeSearchTerms(new[] { "shield heart", "love", "protect", "safe", "safety", "shield", "wishlist" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security" })] ShieldHeart = 0xE574, @@ -7355,7 +7462,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "truck-fast" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "truck fast", "express", "fedex", "mail", "overnight", "package", "ups" })] + [FontAwesomeSearchTerms(new[] { "truck fast", "express", "fedex", "mail", "overnight", "package", "quick", "ups" })] [FontAwesomeCategoriesAttribute(new[] { "Logistics", "Shopping" })] ShippingFast = 0xF48B, @@ -7369,7 +7476,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "shop-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "shop lock", "bodega", "building", "buy", "closed", "lock", "lockdown", "market", "purchase", "quarantine", "shop", "shopping", "store" })] + [FontAwesomeSearchTerms(new[] { "shop lock", "bodega", "building", "buy", "closed", "lock", "lockdown", "market", "padlock", "privacy", "purchase", "quarantine", "shop", "shopping", "store" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Shopping" })] ShopLock = 0xE4A5, @@ -7397,7 +7504,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "shop-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "shop slash", "building", "buy", "closed", "covid-19", "purchase", "shopping" })] + [FontAwesomeSearchTerms(new[] { "shop slash", "building", "buy", "closed", "disabled", "purchase", "shopping" })] [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] ShopSlash = 0xE070, @@ -7418,7 +7525,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "van-shuttle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "van shuttle", "airport", "bus", "machine", "minibus", "public-transportation", "transportation", "travel", "vehicle" })] + [FontAwesomeSearchTerms(new[] { "van shuttle", "airport", "bus", "minibus", "public-transportation", "transportation", "travel", "vehicle" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Transportation", "Travel + Hotel" })] ShuttleVan = 0xF5B6, @@ -7439,7 +7546,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "signature" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "signature", "john hancock", "cursive", "name", "writing" })] + [FontAwesomeSearchTerms(new[] { "signature", "john hancock", "cursive", "name", "username", "writing" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Editing", "Writing" })] Signature = 0xF5B7, @@ -7471,6 +7578,20 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] SimCard = 0xF7C4, + /// + /// The Font Awesome "single-quote-left" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "single quote left", "left single quotation mark", "mention", "note", "phrase", "text", "type" })] + [FontAwesomeCategoriesAttribute(new[] { "Communication", "Punctuation + Symbols", "Writing" })] + SingleQuoteLeft = 0xE81B, + + /// + /// The Font Awesome "single-quote-right" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "single quote right", "mention", "note", "phrase", "right single quotation mark", "text", "type" })] + [FontAwesomeCategoriesAttribute(new[] { "Communication", "Punctuation + Symbols", "Writing" })] + SingleQuoteRight = 0xE81C, + /// /// The Font Awesome "sink" icon unicode character. /// @@ -7488,28 +7609,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-skating" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person skating", "figure skating", "ice", "olympics", "rink", "skate", "winter" })] + [FontAwesomeSearchTerms(new[] { "person skating", "figure skating", "ice", "olympics", "rink", "skate", "uer", "winter" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] Skating = 0xF7C5, /// /// The Font Awesome "person-skiing" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person skiing", "downhill", "olympics", "ski", "skier", "snow", "winter" })] + [FontAwesomeSearchTerms(new[] { "person skiing", "downhill", "olympics", "ski", "skier", "snow", "uer", "winter" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] Skiing = 0xF7C9, /// /// The Font Awesome "person-skiing-nordic" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person skiing nordic", "cross country", "olympics", "winter" })] + [FontAwesomeSearchTerms(new[] { "person skiing nordic", "cross country", "olympics", "uer", "winter" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] SkiingNordic = 0xF7CA, /// /// The Font Awesome "skull" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bones", "death", "face", "fairy tale", "monster", "skeleton", "skull", "x-ray", "yorick" })] + [FontAwesomeSearchTerms(new[] { "bones", "death", "face", "fairy tale", "monster", "skeleton", "skull", "uer", "x-ray", "yorick" })] [FontAwesomeCategoriesAttribute(new[] { "Halloween", "Medical + Health", "Users + People" })] Skull = 0xF54C, @@ -7537,14 +7658,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "sliders" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "adjust", "settings", "sliders", "toggle" })] - [FontAwesomeCategoriesAttribute(new[] { "Editing", "Media Playback", "Music + Audio", "Photos + Images" })] + [FontAwesomeSearchTerms(new[] { "adjust", "configuration", "modify", "settings", "sliders", "toggle" })] + [FontAwesomeCategoriesAttribute(new[] { "Editing", "Media Playback", "Music + Audio", "Photos + Images", "Toggle" })] SlidersH = 0xF1DE, /// /// The Font Awesome "face-smile" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "face smile", "approve", "emoticon", "face", "happy", "rating", "satisfied", "slightly smiling face", "smile" })] + [FontAwesomeSearchTerms(new[] { "face smile", "approve", "default", "emoticon", "face", "happy", "rating", "satisfied", "slightly smiling face", "smile", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Emoji", "Users + People" })] Smile = 0xF118, @@ -7579,21 +7700,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "ban-smoking" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ban smoking", "ban", "cancel", "forbidden", "no", "no smoking", "non-smoking", "not", "prohibited", "smoking" })] + [FontAwesomeSearchTerms(new[] { "ban smoking", "ban", "cancel", "circle", "deny", "disabled", "forbidden", "no", "no smoking", "non-smoking", "not", "prohibited", "slash", "smoking" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Travel + Hotel" })] SmokingBan = 0xF54D, /// /// The Font Awesome "comment-sms" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment sms", "chat", "conversation", "message", "mobile", "notification", "phone", "sms", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment sms", "answer", "chat", "conversation", "message", "mobile", "notification", "phone", "sms", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication" })] Sms = 0xF7CD, /// /// The Font Awesome "person-snowboarding" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person snowboarding", "olympics", "ski", "snow", "snowboard", "snowboarder", "winter" })] + [FontAwesomeSearchTerms(new[] { "person snowboarding", "olympics", "ski", "snow", "snowboard", "snowboarder", "uer", "winter" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] Snowboarding = 0xF7CE, @@ -7691,7 +7812,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-up-wide-short" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up wide short", "arrange", "filter", "order", "sort-amount-desc" })] + [FontAwesomeSearchTerms(new[] { "arrow up wide short", "arrange", "filter", "order", "sort-amount-desc", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] SortAmountUp = 0xF161, @@ -7705,7 +7826,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "sort-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sort down", "arrow", "descending", "filter", "order", "sort-desc" })] + [FontAwesomeSearchTerms(new[] { "sort down", "arrow", "descending", "filter", "insert", "order", "sort-desc" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] SortDown = 0xF0DD, @@ -7740,7 +7861,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "sort-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sort up", "arrow", "ascending", "filter", "order", "sort-asc" })] + [FontAwesomeSearchTerms(new[] { "sort up", "arrow", "ascending", "filter", "order", "sort-asc", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] SortUp = 0xF0DE, @@ -7761,7 +7882,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "spell-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "spell check", "dictionary", "edit", "editor", "grammar", "text" })] + [FontAwesomeSearchTerms(new[] { "spell check", "dictionary", "edit", "editor", "enable", "grammar", "text", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] SpellCheck = 0xF891, @@ -7775,10 +7896,17 @@ public enum FontAwesomeIcon /// /// The Font Awesome "spinner" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "spinner", "circle", "loading", "progress" })] + [FontAwesomeSearchTerms(new[] { "spinner", "circle", "loading", "pending", "progress" })] [FontAwesomeCategoriesAttribute(new[] { "Spinners" })] Spinner = 0xF110, + /// + /// The Font Awesome "spiral" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "spiral", "design", "dizzy", "rotate", "spin", "swirl", "twist" })] + [FontAwesomeCategoriesAttribute(new[] { "Design", "Shapes" })] + Spiral = 0xE80A, + /// /// The Font Awesome "splotch" icon unicode character. /// @@ -7807,6 +7935,13 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] SquareArrowUpRight = 0xF14C, + /// + /// The Font Awesome "square-binary" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "square binary", "ai", "data", "language", "llm", "model", "programming", "token" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Shapes" })] + SquareBinary = 0xE69B, + /// /// The Font Awesome "square-full" icon unicode character. /// @@ -7824,7 +7959,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-person-confined" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square person confined", "captivity", "confined" })] + [FontAwesomeSearchTerms(new[] { "square person confined", "captivity", "confined", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] SquarePersonConfined = 0xE577, @@ -7845,7 +7980,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square xmark", "close", "cross", "cross mark button", "incorrect", "mark", "notice", "notification", "notify", "problem", "square", "window", "wrong", "x", "×" })] + [FontAwesomeSearchTerms(new[] { "square xmark", "close", "cross", "cross mark button", "incorrect", "mark", "notice", "notification", "notify", "problem", "square", "uncheck", "window", "wrong", "x", "×" })] [FontAwesomeCategoriesAttribute(new[] { "Mathematics" })] SquareXmark = 0xF2D3, @@ -7880,7 +8015,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "star" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "achievement", "award", "favorite", "important", "night", "rating", "score", "star" })] + [FontAwesomeSearchTerms(new[] { "achievement", "award", "favorite", "important", "night", "quality", "rating", "score", "star", "vip" })] [FontAwesomeCategoriesAttribute(new[] { "Shapes", "Shopping", "Social", "Toggle" })] Star = 0xF005, @@ -7964,7 +8099,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "stopwatch" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clock", "reminder", "stopwatch", "time" })] + [FontAwesomeSearchTerms(new[] { "clock", "reminder", "stopwatch", "time", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] Stopwatch = 0xF2F2, @@ -7992,7 +8127,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "store-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "store slash", "building", "buy", "closed", "covid-19", "purchase", "shopping" })] + [FontAwesomeSearchTerms(new[] { "store slash", "building", "buy", "closed", "disabled", "purchase", "shopping" })] [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] StoreSlash = 0xE071, @@ -8006,14 +8141,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "street-view" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "street view", "directions", "location", "map", "navigation" })] + [FontAwesomeSearchTerms(new[] { "street view", "directions", "location", "map", "navigation", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Users + People" })] StreetView = 0xF21D, /// /// The Font Awesome "strikethrough" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "strikethrough", "cancel", "edit", "font", "format", "text", "type" })] + [FontAwesomeSearchTerms(new[] { "strikethrough", "cancel", "edit", "font", "format", "modify", "text", "type" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] Strikethrough = 0xF0CC, @@ -8090,7 +8225,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-swimming" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person swimming", "ocean", "person swimming", "pool", "sea", "swim", "water" })] + [FontAwesomeSearchTerms(new[] { "person swimming", "ocean", "person swimming", "pool", "sea", "swim", "uer", "water" })] [FontAwesomeCategoriesAttribute(new[] { "Maritime", "Sports + Fitness", "Travel + Hotel", "Users + People" })] Swimmer = 0xF5C4, @@ -8111,14 +8246,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-rotate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows rotate", "clockwise right and left semicircle arrows", "exchange", "refresh", "reload", "rotate", "swap" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Editing", "Media Playback" })] + [FontAwesomeSearchTerms(new[] { "arrows rotate", "clockwise right and left semicircle arrows", "clockwise", "exchange", "modify", "refresh", "reload", "renew", "retry", "rotate", "swap" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Editing", "Media Playback", "Spinners" })] Sync = 0xF021, /// /// The Font Awesome "rotate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anticlockwise", "arrow", "counterclockwise", "counterclockwise arrows button", "exchange", "refresh", "reload", "rotate", "swap", "withershins" })] + [FontAwesomeSearchTerms(new[] { "arrow", "clockwise", "exchange", "modify", "refresh", "reload", "renew", "retry", "rotate", "swap", "withershins" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Editing", "Media Playback", "Spinners" })] SyncAlt = 0xF2F1, @@ -8132,10 +8267,31 @@ public enum FontAwesomeIcon /// /// The Font Awesome "table" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table", "data", "excel", "spreadsheet" })] + [FontAwesomeSearchTerms(new[] { "table", "category", "data", "excel", "spreadsheet" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Text Formatting" })] Table = 0xF0CE, + /// + /// The Font Awesome "table-cells-column-lock" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "table cells column lock", "blocks", "boxes", "category", "column", "excel", "grid", "lock", "spreadsheet", "squares" })] + [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] + TableCellsColumnLock = 0xE678, + + /// + /// The Font Awesome "table-cells-row-lock" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "table cells row lock", "blocks", "boxes", "category", "column", "column", "excel", "grid", "lock", "lock", "spreadsheet", "squares" })] + [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] + TableCellsRowLock = 0xE67A, + + /// + /// The Font Awesome "table-cells-row-unlock" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "table cells row unlock", "blocks", "boxes", "category", "column", "column", "excel", "grid", "lock", "lock", "spreadsheet", "squares", "unlock" })] + [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] + TableCellsRowUnlock = 0xE691, + /// /// The Font Awesome "tablet" icon unicode character. /// @@ -8175,7 +8331,7 @@ public enum FontAwesomeIcon /// The Font Awesome "gauge-high" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF625. /// - [FontAwesomeSearchTerms(new[] { "gauge high", "dashboard", "fast", "odometer", "speed", "speedometer" })] + [FontAwesomeSearchTerms(new[] { "gauge high", "dashboard", "fast", "odometer", "quick", "speed", "speedometer" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive" })] TachometerAlt = 0xF3FD, @@ -8217,7 +8373,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "list-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "list check", "checklist", "downloading", "downloads", "loading", "progress", "project management", "settings", "to do" })] + [FontAwesomeSearchTerms(new[] { "list check", "bullet", "cheatsheet", "checklist", "downloading", "downloads", "enable", "loading", "progress", "project management", "settings", "summary", "to do", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Text Formatting" })] Tasks = 0xF0AE, @@ -8280,42 +8436,42 @@ public enum FontAwesomeIcon /// /// The Font Awesome "tent" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bivouac", "campground", "refugee", "shelter", "tent" })] + [FontAwesomeSearchTerms(new[] { "bivouac", "campground", "campsite", "refugee", "shelter", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] Tent = 0xE57D, /// /// The Font Awesome "tent-arrow-down-to-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tent arrow down to line", "permanent", "refugee", "shelter" })] + [FontAwesomeSearchTerms(new[] { "tent arrow down to line", "bivouac", "campground", "campsite", "permanent", "refugee", "refugee", "shelter", "shelter", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] TentArrowDownToLine = 0xE57E, /// /// The Font Awesome "tent-arrow-left-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tent arrow left right", "refugee", "shelter", "transition" })] + [FontAwesomeSearchTerms(new[] { "tent arrow left right", "bivouac", "campground", "campsite", "refugee", "refugee", "shelter", "shelter", "tent", "transition" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] TentArrowLeftRight = 0xE57F, /// /// The Font Awesome "tent-arrows-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tent arrows down", "refugee", "shelter", "spontaneous" })] + [FontAwesomeSearchTerms(new[] { "tent arrows down", "bivouac", "campground", "campsite", "insert", "refugee", "refugee", "shelter", "shelter", "spontaneous", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] TentArrowsDown = 0xE581, /// /// The Font Awesome "tent-arrow-turn-left" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tent arrow turn left", "refugee", "shelter", "temporary" })] + [FontAwesomeSearchTerms(new[] { "tent arrow turn left", "bivouac", "campground", "campsite", "refugee", "refugee", "shelter", "shelter", "temporary", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] TentArrowTurnLeft = 0xE580, /// /// The Font Awesome "tents" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tents", "bivouac", "campground", "refugee", "shelter", "tent" })] + [FontAwesomeSearchTerms(new[] { "tents", "bivouac", "bivouac", "campground", "campground", "campsite", "refugee", "refugee", "shelter", "shelter", "tent", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] Tents = 0xE582, @@ -8329,21 +8485,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "text-height" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "text height", "edit", "font", "format", "text", "type" })] + [FontAwesomeSearchTerms(new[] { "text height", "edit", "font", "format", "modify", "text", "type" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] TextHeight = 0xF034, /// /// The Font Awesome "text-width" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "text width", "edit", "font", "format", "text", "type" })] + [FontAwesomeSearchTerms(new[] { "text width", "edit", "font", "format", "modify", "text", "type" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] TextWidth = 0xF035, /// /// The Font Awesome "table-cells" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table cells", "blocks", "boxes", "grid", "squares" })] + [FontAwesomeSearchTerms(new[] { "table cells", "blocks", "boxes", "category", "excel", "grid", "spreadsheet", "squares" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] Th = 0xF00A, @@ -8399,14 +8555,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "table-cells-large" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table cells large", "blocks", "boxes", "grid", "squares" })] + [FontAwesomeSearchTerms(new[] { "table cells large", "blocks", "boxes", "category", "excel", "grid", "spreadsheet", "squares" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ThLarge = 0xF009, /// /// The Font Awesome "table-list" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table list", "checklist", "completed", "done", "finished", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "table list", "category", "cheatsheet", "checklist", "completed", "done", "finished", "ol", "summary", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ThList = 0xF00B, @@ -8431,17 +8587,24 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Maps", "Social", "Writing" })] Thumbtack = 0xF08D, + /// + /// The Font Awesome "thumbtack-slash" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "thumbtack slash", "black pushpin", "coordinates", "location", "marker", "pin", "pushpin", "thumb-tack", "unpin" })] + [FontAwesomeCategoriesAttribute(new[] { "Business", "Maps", "Social", "Writing" })] + ThumbtackSlash = 0xE68F, + /// /// The Font Awesome "ticket" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admission", "admission tickets", "movie", "pass", "support", "ticket" })] + [FontAwesomeSearchTerms(new[] { "admission", "admission tickets", "coupon", "movie", "pass", "support", "ticket", "voucher" })] [FontAwesomeCategoriesAttribute(new[] { "Film + Video", "Maps" })] Ticket = 0xF145, /// /// The Font Awesome "ticket-simple" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ticket simple", "movie", "pass", "support", "ticket" })] + [FontAwesomeSearchTerms(new[] { "ticket simple", "admission", "coupon", "movie", "pass", "support", "ticket", "voucher" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Shapes" })] TicketAlt = 0xF3FF, @@ -8455,29 +8618,29 @@ public enum FontAwesomeIcon /// /// The Font Awesome "xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "xmark", "cancellation x", "multiplication sign", "multiplication x", "cancel", "close", "cross", "cross mark", "error", "exit", "incorrect", "mark", "multiplication", "multiply", "notice", "notification", "notify", "problem", "sign", "wrong", "x", "×" })] + [FontAwesomeSearchTerms(new[] { "xmark", "cancellation x", "multiplication sign", "multiplication x", "cancel", "close", "cross", "cross mark", "error", "exit", "incorrect", "mark", "multiplication", "multiply", "notice", "notification", "notify", "problem", "sign", "uncheck", "wrong", "x", "×" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Mathematics" })] Times = 0xF00D, /// /// The Font Awesome "circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle xmark", "close", "cross", "destroy", "exit", "incorrect", "notice", "notification", "notify", "problem", "wrong", "x" })] + [FontAwesomeSearchTerms(new[] { "circle xmark", "close", "cross", "destroy", "exit", "incorrect", "notice", "notification", "notify", "problem", "uncheck", "wrong", "x" })] [FontAwesomeCategoriesAttribute(new[] { "Mathematics" })] TimesCircle = 0xF057, /// /// The Font Awesome "droplet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "cold", "color", "comic", "drop", "droplet", "raindrop", "sweat", "waterdrop" })] - [FontAwesomeCategoriesAttribute(new[] { "Design", "Humanitarian", "Maps", "Photos + Images" })] + [FontAwesomeSearchTerms(new[] { "blood", "cold", "color", "comic", "drop", "droplet", "raindrop", "sweat", "waterdrop" })] + [FontAwesomeCategoriesAttribute(new[] { "Design", "Humanitarian", "Maps", "Medical + Health", "Photos + Images" })] Tint = 0xF043, /// /// The Font Awesome "droplet-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "droplet slash", "color", "drop", "droplet", "raindrop", "waterdrop" })] - [FontAwesomeCategoriesAttribute(new[] { "Design" })] + [FontAwesomeSearchTerms(new[] { "droplet slash", "blood", "color", "disabled", "drop", "droplet", "raindrop", "waterdrop" })] + [FontAwesomeCategoriesAttribute(new[] { "Design", "Medical + Health" })] TintSlash = 0xF5C7, /// @@ -8518,7 +8681,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "toilet-paper-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "toilet paper slash", "bathroom", "covid-19", "halloween", "holiday", "lavatory", "leaves", "prank", "privy", "restroom", "roll", "toilet", "trouble", "ut oh", "wipe" })] + [FontAwesomeSearchTerms(new[] { "toilet paper slash", "bathroom", "covid-19", "disabled", "halloween", "holiday", "lavatory", "leaves", "prank", "privy", "restroom", "roll", "toilet", "trouble", "ut oh", "wipe" })] [FontAwesomeCategoriesAttribute(new[] { "Household" })] ToiletPaperSlash = 0xE072, @@ -8539,14 +8702,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "toolbox" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "chest", "container", "fix", "mechanic", "repair", "settings", "tool", "toolbox", "tools" })] + [FontAwesomeSearchTerms(new[] { "admin", "chest", "configuration", "container", "equipment", "fix", "maintenance", "mechanic", "modify", "repair", "settings", "tool", "toolbox", "tools" })] [FontAwesomeCategoriesAttribute(new[] { "Construction" })] Toolbox = 0xF552, /// /// The Font Awesome "screwdriver-wrench" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "screwdriver wrench", "admin", "fix", "repair", "screwdriver", "settings", "tools", "wrench" })] + [FontAwesomeSearchTerms(new[] { "screwdriver wrench", "admin", "configuration", "equipment", "fix", "maintenance", "modify", "repair", "screwdriver", "settings", "tools", "wrench" })] [FontAwesomeCategoriesAttribute(new[] { "Construction" })] Tools = 0xF7D9, @@ -8581,7 +8744,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "tower-cell" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tower cell", "airwaves", "antenna", "communication", "radio", "reception", "waves" })] + [FontAwesomeSearchTerms(new[] { "tower cell", "airwaves", "antenna", "communication", "radio", "reception", "signal", "waves" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Connectivity", "Film + Video", "Humanitarian" })] TowerCell = 0xE585, @@ -8609,7 +8772,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "traffic-light" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "traffic light", "direction", "light", "road", "signal", "traffic", "travel", "vertical traffic light" })] + [FontAwesomeSearchTerms(new[] { "traffic light", "direction", "go", "light", "road", "signal", "slow", "stop", "traffic", "travel", "vertical traffic light" })] [FontAwesomeCategoriesAttribute(new[] { "Maps" })] TrafficLight = 0xF637, @@ -8665,21 +8828,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "trash-arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "trash arrow up", "back", "control z", "delete", "garbage", "hide", "oops", "remove", "undo" })] + [FontAwesomeSearchTerms(new[] { "trash arrow up", "back", "control z", "delete", "garbage", "hide", "oops", "remove", "undo", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] TrashRestore = 0xF829, /// /// The Font Awesome "trash-can-arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "trash can arrow up", "back", "control z", "delete", "garbage", "hide", "oops", "remove", "undo" })] + [FontAwesomeSearchTerms(new[] { "trash can arrow up", "back", "control z", "delete", "garbage", "hide", "oops", "remove", "undo", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] TrashRestoreAlt = 0xF82A, /// /// The Font Awesome "tree" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bark", "evergreen tree", "fall", "flora", "forest", "nature", "plant", "seasonal", "tree" })] + [FontAwesomeSearchTerms(new[] { "bark", "evergreen tree", "fall", "flora", "forest", "investment", "nature", "plant", "seasonal", "tree" })] [FontAwesomeCategoriesAttribute(new[] { "Camping", "Maps", "Nature" })] Tree = 0xF1BB, @@ -8700,14 +8863,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "trowel" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "trowel", "build", "construction", "tool" })] + [FontAwesomeSearchTerms(new[] { "trowel", "build", "construction", "equipment", "maintenance", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Humanitarian" })] Trowel = 0xE589, /// /// The Font Awesome "trowel-bricks" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "trowel bricks", "build", "construction", "reconstruction", "tool" })] + [FontAwesomeSearchTerms(new[] { "trowel bricks", "build", "construction", "maintenance", "reconstruction", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Humanitarian" })] TrowelBricks = 0xE58A, @@ -8728,8 +8891,8 @@ public enum FontAwesomeIcon /// /// The Font Awesome "truck-droplet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "truck droplet", "thirst", "truck", "water", "water supply" })] - [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Transportation" })] + [FontAwesomeSearchTerms(new[] { "truck droplet", "blood", "thirst", "truck", "water", "water supply" })] + [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Transportation" })] TruckDroplet = 0xE58C, /// @@ -8777,7 +8940,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "truck-pickup" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "truck pickup", "cargo", "pick-up", "pickup", "pickup truck", "truck", "vehicle" })] + [FontAwesomeSearchTerms(new[] { "truck pickup", "cargo", "maintenance", "pick-up", "pickup", "pickup truck", "truck", "vehicle" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Construction", "Transportation" })] TruckPickup = 0xF63C, @@ -8833,7 +8996,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "underline" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "underline", "edit", "emphasis", "format", "text", "writing" })] + [FontAwesomeSearchTerms(new[] { "underline", "edit", "emphasis", "format", "modify", "text", "writing" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] Underline = 0xF0CD, @@ -8841,20 +9004,20 @@ public enum FontAwesomeIcon /// The Font Awesome "arrow-rotate-left" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "arrow rotate left", "anticlockwise open circle arrow", "back", "control z", "exchange", "oops", "return", "rotate", "swap" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback", "Spinners" })] Undo = 0xF0E2, /// /// The Font Awesome "rotate-left" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "rotate left", "back", "control z", "exchange", "oops", "return", "swap" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback", "Spinners" })] UndoAlt = 0xF2EA, /// /// The Font Awesome "universal-access" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "universal access", "users-people" })] + [FontAwesomeSearchTerms(new[] { "universal access", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility" })] UniversalAccess = 0xF29A, @@ -8868,252 +9031,254 @@ public enum FontAwesomeIcon /// /// The Font Awesome "link-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "link slash", "attachment", "chain", "chain-broken", "remove" })] + [FontAwesomeSearchTerms(new[] { "link slash", "attachment", "chain", "chain-broken", "disabled", "disconnect", "remove" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] Unlink = 0xF127, /// /// The Font Awesome "unlock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "lock", "open", "password", "private", "protect", "unlock", "unlocked" })] + [FontAwesomeSearchTerms(new[] { "admin", "lock", "open", "padlock", "password", "privacy", "private", "protect", "unlock", "unlocked" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] Unlock = 0xF09C, /// /// The Font Awesome "unlock-keyhole" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "unlock keyhole", "admin", "lock", "password", "private", "protect" })] + [FontAwesomeSearchTerms(new[] { "unlock keyhole", "admin", "lock", "padlock", "password", "privacy", "private", "protect" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] UnlockAlt = 0xF13E, /// /// The Font Awesome "upload" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "upload", "hard drive", "import", "publish" })] + [FontAwesomeSearchTerms(new[] { "upload", "hard drive", "import", "publish", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Devices + Hardware" })] Upload = 0xF093, /// /// The Font Awesome "user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user", "adult", "bust", "bust in silhouette", "gender-neutral", "person", "profile", "silhouette", "unspecified gender", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user", "adult", "bust", "bust in silhouette", "default", "employee", "gender-neutral", "person", "profile", "silhouette", "uer", "unspecified gender", "username", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] User = 0xF007, /// - /// The Font Awesome "user-large" icon unicode character. + /// The Font Awesome "user" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF007. /// - [FontAwesomeSearchTerms(new[] { "user large", "users-people" })] - [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] + [FontAwesomeSearchTerms(new[] { "user", "adult", "bust", "bust in silhouette", "default", "employee", "gender-neutral", "person", "profile", "silhouette", "uer", "unspecified gender", "username", "users-people" })] + [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] UserAlt = 0xF406, /// - /// The Font Awesome "user-large-slash" icon unicode character. + /// The Font Awesome "user-slash" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF506. /// - [FontAwesomeSearchTerms(new[] { "user large slash", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user slash", "ban", "delete", "deny", "disabled", "disconnect", "employee", "remove", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserAltSlash = 0xF4FA, /// /// The Font Awesome "user-astronaut" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user astronaut", "avatar", "clothing", "cosmonaut", "nasa", "space", "suit" })] + [FontAwesomeSearchTerms(new[] { "user astronaut", "avatar", "clothing", "cosmonaut", "nasa", "space", "suit", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Science Fiction", "Users + People" })] UserAstronaut = 0xF4FB, /// /// The Font Awesome "user-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user check", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user check", "employee", "enable", "uer", "users-people", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserCheck = 0xF4FC, /// /// The Font Awesome "circle-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle user", "users-people" })] + [FontAwesomeSearchTerms(new[] { "circle user", "employee", "uer", "username", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] UserCircle = 0xF2BD, /// /// The Font Awesome "user-clock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user clock", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user clock", "employee", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserClock = 0xF4FD, /// /// The Font Awesome "user-gear" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user gear", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user gear", "employee", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserCog = 0xF4FE, /// /// The Font Awesome "user-pen" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user pen", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user pen", "employee", "modify", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserEdit = 0xF4FF, /// /// The Font Awesome "user-group" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user group", "bust", "busts in silhouette", "silhouette", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user group", "bust", "busts in silhouette", "crowd", "employee", "silhouette", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] UserFriends = 0xF500, /// /// The Font Awesome "user-graduate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user graduate", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user graduate", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Users + People" })] UserGraduate = 0xF501, /// /// The Font Awesome "user-injured" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user injured", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user injured", "employee", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UserInjured = 0xF728, /// /// The Font Awesome "user-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user lock", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user lock", "employee", "padlock", "privacy", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Security", "Users + People" })] UserLock = 0xF502, /// /// The Font Awesome "user-doctor" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user doctor", "covid-19", "health", "job", "medical", "nurse", "occupation", "physician", "profile", "surgeon", "worker" })] + [FontAwesomeSearchTerms(new[] { "user doctor", "covid-19", "health", "job", "medical", "nurse", "occupation", "physician", "profile", "surgeon", "uer", "worker" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Users + People" })] UserMd = 0xF0F0, /// /// The Font Awesome "user-minus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user minus", "delete", "negative", "remove" })] + [FontAwesomeSearchTerms(new[] { "user minus", "delete", "employee", "negative", "remove", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserMinus = 0xF503, /// /// The Font Awesome "user-ninja" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user ninja", "assassin", "avatar", "dangerous", "deadly", "fighter", "hidden", "ninja", "sneaky", "stealth" })] + [FontAwesomeSearchTerms(new[] { "user ninja", "assassin", "avatar", "dangerous", "deadly", "fighter", "hidden", "ninja", "sneaky", "stealth", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserNinja = 0xF504, /// /// The Font Awesome "user-nurse" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user nurse", "covid-19", "doctor", "health", "md", "medical", "midwife", "physician", "practitioner", "surgeon", "worker" })] + [FontAwesomeSearchTerms(new[] { "user nurse", "covid-19", "doctor", "health", "md", "medical", "midwife", "physician", "practitioner", "surgeon", "uer", "worker" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] UserNurse = 0xF82F, /// /// The Font Awesome "user-plus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user plus", "add", "avatar", "positive", "sign up", "signup", "team" })] + [FontAwesomeSearchTerms(new[] { "user plus", "add", "avatar", "employee", "follow", "positive", "sign up", "signup", "team", "user" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] UserPlus = 0xF234, /// /// The Font Awesome "users" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users", "users-people" })] + [FontAwesomeSearchTerms(new[] { "users", "employee", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] Users = 0xF0C0, /// /// The Font Awesome "users-between-lines" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users between lines", "covered", "group", "people" })] + [FontAwesomeSearchTerms(new[] { "users between lines", "covered", "crowd", "employee", "group", "people", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersBetweenLines = 0xE591, /// /// The Font Awesome "users-gear" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users gear", "users-people" })] + [FontAwesomeSearchTerms(new[] { "users gear", "employee", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UsersCog = 0xF509, /// /// The Font Awesome "user-secret" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user secret", "detective", "sleuth", "spy", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user secret", "detective", "sleuth", "spy", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Security", "Users + People" })] UserSecret = 0xF21B, /// /// The Font Awesome "user-shield" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user shield", "protect", "safety" })] + [FontAwesomeSearchTerms(new[] { "user shield", "employee", "protect", "safety", "security", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Security", "Users + People" })] UserShield = 0xF505, /// /// The Font Awesome "user-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user slash", "ban", "delete", "remove" })] + [FontAwesomeSearchTerms(new[] { "user slash", "ban", "delete", "deny", "disabled", "disconnect", "employee", "remove", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserSlash = 0xF506, /// /// The Font Awesome "users-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users line", "group", "need", "people" })] + [FontAwesomeSearchTerms(new[] { "users line", "crowd", "employee", "group", "need", "people", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersLine = 0xE592, /// /// The Font Awesome "users-rays" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users rays", "affected", "focused", "group", "people" })] + [FontAwesomeSearchTerms(new[] { "users rays", "affected", "crowd", "employee", "focused", "group", "people", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersRays = 0xE593, /// /// The Font Awesome "users-rectangle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users rectangle", "focus", "group", "people", "reached" })] + [FontAwesomeSearchTerms(new[] { "users rectangle", "crowd", "employee", "focus", "group", "people", "reached", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersRectangle = 0xE594, /// /// The Font Awesome "users-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users slash", "users-people" })] + [FontAwesomeSearchTerms(new[] { "users slash", "disabled", "disconnect", "employee", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UsersSlash = 0xE073, /// /// The Font Awesome "users-viewfinder" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users viewfinder", "focus", "group", "people", "targeted" })] + [FontAwesomeSearchTerms(new[] { "users viewfinder", "crowd", "focus", "group", "people", "targeted", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersViewfinder = 0xE595, /// /// The Font Awesome "user-tag" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user tag", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user tag", "employee", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserTag = 0xF507, /// /// The Font Awesome "user-tie" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user tie", "avatar", "business", "clothing", "formal", "professional", "suit" })] + [FontAwesomeSearchTerms(new[] { "user tie", "administrator", "avatar", "business", "clothing", "employee", "formal", "offer", "portfolio", "professional", "suit", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Clothing + Fashion", "Users + People" })] UserTie = 0xF508, /// /// The Font Awesome "user-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user xmark", "archive", "delete", "remove", "x" })] + [FontAwesomeSearchTerms(new[] { "user xmark", "archive", "delete", "employee", "remove", "uer", "uncheck", "x" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserTimes = 0xF235, @@ -9134,15 +9299,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "vault" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vault", "bank", "important", "lock", "money", "safe" })] + [FontAwesomeSearchTerms(new[] { "vault", "bank", "important", "investment", "lock", "money", "premium", "privacy", "safe", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Money", "Security" })] Vault = 0xE2C5, /// - /// The Font Awesome "vector-square" icon unicode character. + /// The Font Awesome "vectorsquare" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vector square", "anchors", "lines", "object", "render", "shape" })] - [FontAwesomeCategoriesAttribute(new[] { "Design" })] + [Obsolete] VectorSquare = 0xF5CB, /// @@ -9183,21 +9347,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "vial" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vial", "ampule", "chemist", "chemistry", "experiment", "lab", "sample", "science", "test", "test tube" })] + [FontAwesomeSearchTerms(new[] { "vial", "ampule", "chemist", "chemistry", "experiment", "knowledge", "lab", "sample", "science", "test", "test tube" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Science" })] Vial = 0xF492, /// /// The Font Awesome "vial-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vial circle check", "ampule", "chemist", "chemistry", "not affected", "ok", "okay", "success", "test tube", "tube", "vaccine" })] + [FontAwesomeSearchTerms(new[] { "vial circle check", "ampule", "chemist", "chemistry", "enable", "not affected", "ok", "okay", "success", "test tube", "tube", "vaccine", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Science" })] VialCircleCheck = 0xE596, /// /// The Font Awesome "vials" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vials", "ampule", "experiment", "lab", "sample", "science", "test", "test tube" })] + [FontAwesomeSearchTerms(new[] { "vials", "ampule", "experiment", "knowledge", "lab", "sample", "science", "test", "test tube" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Science" })] Vials = 0xF493, @@ -9218,7 +9382,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "video-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "video slash", "add", "create", "film", "new", "positive", "record", "video" })] + [FontAwesomeSearchTerms(new[] { "video slash", "add", "create", "disabled", "disconnect", "film", "new", "positive", "record", "video" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video" })] VideoSlash = 0xF4E2, @@ -9246,7 +9410,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "virus-covid-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "virus covid slash", "bug", "covid-19", "flu", "health", "infection", "pandemic", "vaccine", "viral", "virus" })] + [FontAwesomeSearchTerms(new[] { "virus covid slash", "bug", "covid-19", "disabled", "flu", "health", "infection", "pandemic", "vaccine", "viral", "virus" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] VirusCovidSlash = 0xE4A9, @@ -9260,7 +9424,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "virus-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "virus slash", "bug", "coronavirus", "covid-19", "cure", "eliminate", "flu", "health", "infection", "pandemic", "sick", "vaccine", "viral" })] + [FontAwesomeSearchTerms(new[] { "virus slash", "bug", "coronavirus", "covid-19", "cure", "disabled", "eliminate", "flu", "health", "infection", "pandemic", "sick", "vaccine", "viral" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] VirusSlash = 0xE075, @@ -9316,7 +9480,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "check-to-slot" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "check to slot", "accept", "cast", "election", "politics", "positive", "voting", "yes" })] + [FontAwesomeSearchTerms(new[] { "check to slot", "accept", "cast", "election", "enable", "politics", "positive", "validate", "voting", "working", "yes" })] [FontAwesomeCategoriesAttribute(new[] { "Political" })] VoteYea = 0xF772, @@ -9337,14 +9501,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-walking" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking", "crosswalk", "exercise", "hike", "move", "person walking", "walk", "walking" })] + [FontAwesomeSearchTerms(new[] { "person walking", "crosswalk", "exercise", "follow", "hike", "move", "person walking", "uer", "walk", "walking", "workout" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Sports + Fitness", "Users + People" })] Walking = 0xF554, /// /// The Font Awesome "wallet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wallet", "billfold", "cash", "currency", "money" })] + [FontAwesomeSearchTerms(new[] { "wallet", "billfold", "cash", "currency", "money", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Money" })] Wallet = 0xF555, @@ -9380,9 +9544,16 @@ public enum FontAwesomeIcon /// The Font Awesome "wave-square" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "wave square", "frequency", "pulse", "signal" })] - [FontAwesomeCategoriesAttribute(new[] { "Mathematics" })] + [FontAwesomeCategoriesAttribute(new[] { "Mathematics", "Music + Audio" })] WaveSquare = 0xF83E, + /// + /// The Font Awesome "web-awesome" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "web awesome", "awesome", "coding", "components", "crown", "web" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Design" })] + WebAwesome = 0xE682, + /// /// The Font Awesome "weight-scale" icon unicode character. /// @@ -9407,28 +9578,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "wheat-awn-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wheat awn circle exclamation", "affected", "famine", "food", "gluten", "hunger", "starve", "straw" })] + [FontAwesomeSearchTerms(new[] { "wheat awn circle exclamation", "affected", "failed", "famine", "food", "gluten", "hunger", "starve", "straw" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Food + Beverage", "Humanitarian" })] WheatAwnCircleExclamation = 0xE598, /// /// The Font Awesome "wheelchair" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wheelchair", "users-people" })] + [FontAwesomeSearchTerms(new[] { "wheelchair", "disabled", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Maps", "Medical + Health", "Transportation", "Travel + Hotel", "Users + People" })] Wheelchair = 0xF193, /// /// The Font Awesome "wheelchair-move" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wheelchair move", "access", "handicap", "impairment", "physical", "wheelchair symbol" })] + [FontAwesomeSearchTerms(new[] { "wheelchair move", "access", "disabled", "handicap", "impairment", "physical", "uer", "wheelchair symbol" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Humanitarian", "Maps", "Medical + Health", "Transportation", "Travel + Hotel", "Users + People" })] WheelchairMove = 0xE2CE, /// /// The Font Awesome "wifi" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wifi", "connection", "hotspot", "internet", "network", "wireless" })] + [FontAwesomeSearchTerms(new[] { "wifi", "connection", "hotspot", "internet", "network", "signal", "wireless", "www" })] [FontAwesomeCategoriesAttribute(new[] { "Connectivity", "Humanitarian", "Maps", "Toggle", "Travel + Hotel" })] Wifi = 0xF1EB, @@ -9442,7 +9613,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "rectangle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "rectangle xmark", "browser", "cancel", "computer", "development" })] + [FontAwesomeSearchTerms(new[] { "rectangle xmark", "browser", "cancel", "computer", "development", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Coding" })] WindowClose = 0xF410, @@ -9505,7 +9676,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "wrench" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "construction", "fix", "mechanic", "plumbing", "settings", "spanner", "tool", "update", "wrench" })] + [FontAwesomeSearchTerms(new[] { "configuration", "construction", "equipment", "fix", "mechanic", "modify", "plumbing", "settings", "spanner", "tool", "update", "wrench" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Maps" })] Wrench = 0xF0AD, From 624191d1e03a052e5eafe56bd921f1f0623e3969 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 16:45:40 +0100 Subject: [PATCH 290/477] Update DalamudAssetPath to FontAwesome710FreeSolid.otf --- Dalamud/DalamudAsset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index 27771116e..e234fbb4c 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -151,7 +151,7 @@ public enum DalamudAsset /// : FontAwesome Free Solid. /// [DalamudAsset(DalamudAssetPurpose.Font)] - [DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")] + [DalamudAssetPath("UIRes", "FontAwesome710FreeSolid.otf")] FontAwesomeFreeSolid = 2003, /// From 2029a0f8a69e0d10b32b8795ce180ebf3c6eb8e5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 21:31:25 +0100 Subject: [PATCH 291/477] Also add fallback for SeStringDrawState.ScreenOffset for now, make sure that it is populated --- .../SeStringDrawParams.cs | 4 +++- .../ImGuiSeStringRenderer/SeStringDrawState.cs | 3 ++- .../Data/Widgets/SeStringRendererTestWidget.cs | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs index 972013328..09c3e9ed9 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs @@ -25,7 +25,9 @@ public record struct SeStringDrawParams public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; } /// Gets or sets the screen offset of the left top corner. - /// Screen offset to draw at, or null to use . + /// Screen offset to draw at, or null to use , if no + /// is specified. Otherwise, you must specify it (for example, by passing when passing the window + /// draw list. public Vector2? ScreenOffset { get; set; } /// Gets or sets the font to use. diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 5e63ef160..5601100e9 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -63,11 +63,12 @@ public unsafe ref struct SeStringDrawState else { this.drawList = ssdp.TargetDrawList.Value; - this.ScreenOffset = Vector2.Zero; + this.ScreenOffset = ssdp.ScreenOffset ?? Vector2.Zero; // API14: Remove, always throw if (ThreadSafety.IsMainThread) { + this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos(); this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); } else diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs index 0f51e0322..6a07152e5 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs @@ -177,6 +177,24 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget ImGuiHelpers.SeStringWrapped(this.logkind.Value.Data.Span, this.style); } + if (ImGui.CollapsingHeader("Draw into drawlist")) + { + ImGuiHelpers.ScaledDummy(100); + ImGui.SetCursorScreenPos(ImGui.GetItemRectMin() + ImGui.GetStyle().FramePadding); + var clipMin = ImGui.GetItemRectMin() + ImGui.GetStyle().FramePadding; + var clipMax = ImGui.GetItemRectMax() - ImGui.GetStyle().FramePadding; + clipMin.Y = MathF.Max(clipMin.Y, ImGui.GetWindowPos().Y); + clipMax.Y = MathF.Min(clipMax.Y, ImGui.GetWindowPos().Y + ImGui.GetWindowHeight()); + + var dl = ImGui.GetWindowDrawList(); + dl.PushClipRect(clipMin, clipMax); + ImGuiHelpers.CompileSeStringWrapped( + "Test test", + new SeStringDrawParams + { Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = dl }); + dl.PopClipRect(); + } + if (ImGui.CollapsingHeader("Addon Table"u8)) { if (ImGui.BeginTable("Addon Sheet"u8, 3)) From c45c6aafe1a8a8561826155aae0efc46b478a2d8 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 21:57:54 +0100 Subject: [PATCH 292/477] Don't consider failed index integrity checks as having "modified game data files" --- Dalamud/Data/DataManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index ed0aa6c4d..559bd84dc 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -82,8 +82,10 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager var tsInfo = JsonConvert.DeserializeObject( dalamud.StartInfo.TroubleshootingPackData); + + // Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout this.HasModifiedGameDataFiles = - tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception; + tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed; if (this.HasModifiedGameDataFiles) Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData); From 8ed1af30dfa33c892fa1b0446054e512a4d0a760 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 22:55:16 +0100 Subject: [PATCH 293/477] build: 13.0.0.14 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a50f12d79..a4b203406 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.13 + 13.0.0.14 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 2dbae055226d1dfb85662b2d68df92926a6ea343 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 7 Dec 2025 16:45:59 -0800 Subject: [PATCH 294/477] Add very thurough exception handling --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 498 +++++++++++------- 1 file changed, 318 insertions(+), 180 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 8fbf77534..47ff92c3d 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -136,14 +136,30 @@ internal unsafe class AddonVirtualTable : IDisposable private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) { - this.LogEvent(EnableLogging); + AtkEventListener* result = null; - var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); - - if ((freeFlags & 1) == 1) + try { - IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); - AddonLifecycle.AllocatedTables.Remove(this); + this.LogEvent(EnableLogging); + + try + { + result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Dtor. This may be a bug in the game or another plugin hooking this method."); + } + + if ((freeFlags & 1) == 1) + { + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + AddonLifecycle.AllocatedTables.Remove(this); + } + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDestructor."); } return result; @@ -151,338 +167,460 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(EnableLogging); - - this.setupArgs.Addon = addon; - this.setupArgs.AtkValueCount = valueCount; - this.setupArgs.AtkValues = (nint)values; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs); - - valueCount = this.setupArgs.AtkValueCount; - values = (AtkValue*)this.setupArgs.AtkValues; - try { - this.originalVirtualTable->OnSetup(addon, valueCount, values); + this.LogEvent(EnableLogging); + + this.setupArgs.Addon = addon; + this.setupArgs.AtkValueCount = valueCount; + this.setupArgs.AtkValues = (nint)values; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs); + + valueCount = this.setupArgs.AtkValueCount; + values = (AtkValue*)this.setupArgs.AtkValues; + + try + { + this.originalVirtualTable->OnSetup(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnSetup. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonSetup."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs); } private void OnAddonFinalize(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.finalizeArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs); - try { - this.originalVirtualTable->Finalizer(thisPtr); + this.LogEvent(EnableLogging); + + this.finalizeArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs); + + try + { + this.originalVirtualTable->Finalizer(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Finalizer. This may be a bug in the game or another plugin hooking this method."); + } } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFinalize."); } } private void OnAddonDraw(AtkUnitBase* addon) { - this.LogEvent(EnableLogging); - - this.drawArgs.Addon = addon; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs); - try { - this.originalVirtualTable->Draw(addon); + this.LogEvent(EnableLogging); + + this.drawArgs.Addon = addon; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs); + + try + { + this.originalVirtualTable->Draw(addon); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Draw. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDraw."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs); } private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.LogEvent(EnableLogging); - - this.updateArgs.Addon = addon; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs); - - // Note: Do not pass or allow manipulation of delta. - // It's realistically not something that should be needed. - try { - this.originalVirtualTable->Update(addon, delta); + this.LogEvent(EnableLogging); + + this.updateArgs.Addon = addon; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs); + + // Note: Do not pass or allow manipulation of delta. + // It's realistically not something that should be needed. + // And even if someone does, they are encouraged to hook Update themselves. + + try + { + this.originalVirtualTable->Update(addon, delta); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Update. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonUpdate."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs); } private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(EnableLogging); - var result = false; - this.refreshArgs.Addon = addon; - this.refreshArgs.AtkValueCount = valueCount; - this.refreshArgs.AtkValues = (nint)values; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs); - - valueCount = this.refreshArgs.AtkValueCount; - values = (AtkValue*)this.refreshArgs.AtkValues; - try { - result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + this.LogEvent(EnableLogging); + + this.refreshArgs.Addon = addon; + this.refreshArgs.AtkValueCount = valueCount; + this.refreshArgs.AtkValues = (nint)values; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs); + + valueCount = this.refreshArgs.AtkValueCount; + values = (AtkValue*)this.refreshArgs.AtkValues; + + try + { + result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnRefresh. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonRefresh."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs); return result; } private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.LogEvent(EnableLogging); - - this.requestedUpdateArgs.Addon = addon; - this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData; - this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs); - - numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData; - stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData; - try { - this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + this.LogEvent(EnableLogging); + + this.requestedUpdateArgs.Addon = addon; + this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData; + this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs); + + numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData; + stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData; + + try + { + this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnRequestedUpdate."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs); } private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - this.LogEvent(EnableLogging); - - this.receiveEventArgs.Addon = (nint)addon; - this.receiveEventArgs.AtkEventType = (byte)eventType; - this.receiveEventArgs.EventParam = eventParam; - this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent; - this.receiveEventArgs.AtkEventData = (nint)atkEventData; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs); - - eventType = (AtkEventType)this.receiveEventArgs.AtkEventType; - eventParam = this.receiveEventArgs.EventParam; - atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent; - atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData; - try { - this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + this.LogEvent(EnableLogging); + + this.receiveEventArgs.Addon = (nint)addon; + this.receiveEventArgs.AtkEventType = (byte)eventType; + this.receiveEventArgs.EventParam = eventParam; + this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent; + this.receiveEventArgs.AtkEventData = (nint)atkEventData; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs); + + eventType = (AtkEventType)this.receiveEventArgs.AtkEventType; + eventParam = this.receiveEventArgs.EventParam; + atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent; + atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData; + + try + { + this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon ReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonReceiveEvent."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs); } private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) { - this.LogEvent(EnableLogging); - var result = false; - this.openArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs); - try { - result = this.originalVirtualTable->Open(thisPtr, depthLayer); + this.LogEvent(EnableLogging); + + this.openArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs); + + try + { + result = this.originalVirtualTable->Open(thisPtr, depthLayer); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Open. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonOpen."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs); - return result; } private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) { - this.LogEvent(EnableLogging); - var result = false; - this.closeArgs.Addon = thisPtr; - this.closeArgs.FireCallback = fireCallback; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs); - - fireCallback = this.closeArgs.FireCallback; - try { - result = this.originalVirtualTable->Close(thisPtr, fireCallback); + this.LogEvent(EnableLogging); + + this.closeArgs.Addon = thisPtr; + this.closeArgs.FireCallback = fireCallback; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs); + + fireCallback = this.closeArgs.FireCallback; + + try + { + result = this.originalVirtualTable->Close(thisPtr, fireCallback); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Close. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonClose."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs); - return result; } private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) { - this.LogEvent(EnableLogging); - - this.showArgs.Addon = thisPtr; - this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect; - this.showArgs.UnsetShowHideFlags = unsetShowHideFlags; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs); - - silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect; - unsetShowHideFlags = this.showArgs.UnsetShowHideFlags; - try { - this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + this.LogEvent(EnableLogging); + + this.showArgs.Addon = thisPtr; + this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect; + this.showArgs.UnsetShowHideFlags = unsetShowHideFlags; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs); + + silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect; + unsetShowHideFlags = this.showArgs.UnsetShowHideFlags; + + try + { + this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Show. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonShow."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs); } private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) { - this.LogEvent(EnableLogging); - - this.hideArgs.Addon = thisPtr; - this.hideArgs.UnknownBool = unkBool; - this.hideArgs.CallHideCallback = callHideCallback; - this.hideArgs.SetShowHideFlags = setShowHideFlags; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs); - - unkBool = this.hideArgs.UnknownBool; - callHideCallback = this.hideArgs.CallHideCallback; - setShowHideFlags = this.hideArgs.SetShowHideFlags; - try { - this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + this.LogEvent(EnableLogging); + + this.hideArgs.Addon = thisPtr; + this.hideArgs.UnknownBool = unkBool; + this.hideArgs.CallHideCallback = callHideCallback; + this.hideArgs.SetShowHideFlags = setShowHideFlags; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs); + + unkBool = this.hideArgs.UnknownBool; + callHideCallback = this.hideArgs.CallHideCallback; + setShowHideFlags = this.hideArgs.SetShowHideFlags; + + try + { + this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Hide. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonHide."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); } private void OnAddonMove(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.onMoveArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs); - try { - this.originalVirtualTable->OnMove(thisPtr); + this.LogEvent(EnableLogging); + + this.onMoveArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs); + + try + { + this.originalVirtualTable->OnMove(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnMove. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnAddonMove. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMove."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs); } private void OnAddonMouseOver(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.onMouseOverArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs); - try { - this.originalVirtualTable->OnMouseOver(thisPtr); + this.LogEvent(EnableLogging); + + this.onMouseOverArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs); + + try + { + this.originalVirtualTable->OnMouseOver(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnMouseOver. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnAddonMouseOver. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOver."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs); } private void OnAddonMouseOut(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.onMouseOutArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs); - try { - this.originalVirtualTable->OnMouseOut(thisPtr); + this.LogEvent(EnableLogging); + + this.onMouseOutArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs); + + try + { + this.originalVirtualTable->OnMouseOut(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnMouseOut. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnAddonMouseOut. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOut."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs); } private void OnAddonFocus(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.focusArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs); - try { - this.originalVirtualTable->Focus(thisPtr); + this.LogEvent(EnableLogging); + + this.focusArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs); + + try + { + this.originalVirtualTable->Focus(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Focus. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnAddonFocus. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocus."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs); } [Conditional("DEBUG")] From d0110f7251d50b2a45fbc51d6a8b03662e7afa54 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 20:03:22 +0100 Subject: [PATCH 295/477] Hardcode HasModifiedGameDataFiles to false for now until XL is fixed --- Dalamud/Data/DataManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 559bd84dc..f53195a2d 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -84,8 +84,11 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager dalamud.StartInfo.TroubleshootingPackData); // Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout - this.HasModifiedGameDataFiles = - tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed; + // this.HasModifiedGameDataFiles = + // tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed; + + // TODO: Put above back when check in XL is fixed + this.HasModifiedGameDataFiles = false; if (this.HasModifiedGameDataFiles) Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData); From 5d08170333d4e460b3eeb5197e286df86d5bdffe Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 20:03:43 +0100 Subject: [PATCH 296/477] Keep rendering title bar buttons if one is not available clickthrough --- Dalamud/Interface/Windowing/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index b0786fbb5..7ecd5e15c 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -864,7 +864,7 @@ public abstract class Window foreach (var button in this.allButtons) { if (this.internalIsClickthrough && !button.AvailableClickthrough) - return; + continue; Vector2 position = new(titleBarRect.Max.X - padR - buttonSize, titleBarRect.Min.Y + style.FramePadding.Y); padR += buttonSize + style.ItemInnerSpacing.X; From 24caa1cb18f47c705b6706e718a645f3d25c056f Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 20:05:14 +0100 Subject: [PATCH 297/477] PresetWindow.IsDefault can be JsonIgnore --- Dalamud/Interface/Windowing/Persistence/PresetModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Windowing/Persistence/PresetModel.cs b/Dalamud/Interface/Windowing/Persistence/PresetModel.cs index f7910e0b2..4ddf55e51 100644 --- a/Dalamud/Interface/Windowing/Persistence/PresetModel.cs +++ b/Dalamud/Interface/Windowing/Persistence/PresetModel.cs @@ -53,6 +53,7 @@ internal class PresetModel /// /// Gets a value indicating whether this preset is in the default state. /// + [JsonIgnore] public bool IsDefault => !this.IsPinned && !this.IsClickThrough && From 2806e59dba4562e75f7721e40fd3d2bdd3935577 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 20:09:31 +0100 Subject: [PATCH 298/477] Also remove borders for dev bar, to prevent themes from causing weirdness --- Dalamud/Interface/Internal/DalamudInterface.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index bf55a5486..b0fbeb6c5 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -669,6 +669,8 @@ internal class DalamudInterface : IInternalDisposableService { using var barColor = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0.060f, 0.060f, 0.060f, 0.773f)); barColor.Push(ImGuiCol.MenuBarBg, Vector4.Zero); + barColor.Push(ImGuiCol.Border, Vector4.Zero); + barColor.Push(ImGuiCol.BorderShadow, Vector4.Zero); if (ImGui.BeginMainMenuBar()) { var pluginManager = Service.Get(); From 97df73acea61ddf94c135bc37e4f402b6e5a6cab Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 21:00:08 +0100 Subject: [PATCH 299/477] Ensure that we don't catch mouse up events without corresponding mouse down events Fixes an issue wherein the cursor could get locked by the game if WantCaptureMouse becomes true in between down and up events --- .../InputHandler/Win32InputHandler.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs index 0b2e27b57..6b26ce37d 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using System.Text; using Dalamud.Bindings.ImGui; +using Dalamud.Console; using Dalamud.Memory; using Dalamud.Utility; @@ -37,6 +38,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler private readonly WndProcDelegate wndProcDelegate; private readonly nint platformNamePtr; + private readonly IConsoleVariable cvLogMouseEvents; + private ViewportHandler viewportHandler; private int mouseButtonsDown; @@ -87,6 +90,11 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler this.cursors[(int)ImGuiMouseCursor.ResizeNwse] = LoadCursorW(default, IDC.IDC_SIZENWSE); this.cursors[(int)ImGuiMouseCursor.Hand] = LoadCursorW(default, IDC.IDC_HAND); this.cursors[(int)ImGuiMouseCursor.NotAllowed] = LoadCursorW(default, IDC.IDC_NO); + + this.cvLogMouseEvents = Service.Get().AddVariable( + "imgui.log_mouse_events", + "Log mouse events to console for debugging", + false); } /// @@ -267,11 +275,23 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler case WM.WM_XBUTTONDOWN: case WM.WM_XBUTTONDBLCLK: { + if (this.cvLogMouseEvents.Value) + { + Log.Verbose( + "Handle MouseDown {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}", + GetButton(msg, wParam), + io.WantCaptureMouse, + this.mouseButtonsDown); + } + var button = GetButton(msg, wParam); if (io.WantCaptureMouse) { if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero) + { SetCapture(hWndCurrent); + } + this.mouseButtonsDown |= 1 << button; io.AddMouseButtonEvent(button, true); return default(LRESULT); @@ -288,12 +308,28 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler case WM.WM_MBUTTONUP: case WM.WM_XBUTTONUP: { + if (this.cvLogMouseEvents.Value) + { + Log.Verbose( + "Handle MouseUp {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}", + GetButton(msg, wParam), + io.WantCaptureMouse, + this.mouseButtonsDown); + } + var button = GetButton(msg, wParam); - if (io.WantCaptureMouse) + + // Need to check if we captured the button event away from the game here, otherwise the game might get + // a down event but no up event, causing the cursor to get stuck. + // Can happen if WantCaptureMouse becomes true in between down and up + if (io.WantCaptureMouse && (this.mouseButtonsDown & (1 << button)) != 0) { this.mouseButtonsDown &= ~(1 << button); if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent) + { ReleaseCapture(); + } + io.AddMouseButtonEvent(button, false); return default(LRESULT); } From e53ccdbcc03a4931e4594491c6250395e5a3a3e5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 9 Dec 2025 00:18:28 +0100 Subject: [PATCH 300/477] build: 13.0.0.15 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a4b203406..5aca47e0c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.14 + 13.0.0.15 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From b88a6bb61646d32bf2ca2df54b611f58a81a5e5b Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Wed, 10 Dec 2025 23:12:44 +0900 Subject: [PATCH 301/477] Always pop DalamudStandard style if pushed earlier in Draw --- Dalamud/Interface/Windowing/Window.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 7ecd5e15c..ed9318e49 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -672,16 +672,13 @@ public abstract class Window Task.FromResult(tex)); } - if (!this.hasError) + if (isErrorStylePushed) { - this.PostDraw(); + Style.StyleModelV1.DalamudStandard.Pop(); } else { - if (isErrorStylePushed) - { - Style.StyleModelV1.DalamudStandard.Pop(); - } + this.PostDraw(); } this.PostHandlePreset(persistence); From 201c9cfcf25c578f3bb2a3964b8674708b35d634 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 10 Dec 2025 01:42:45 +0100 Subject: [PATCH 302/477] Use game window to calculate offsets in fallback mouse position code --- .../ImGuiBackend/InputHandler/Win32InputHandler.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs index 6b26ce37d..8417a90e5 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs @@ -494,7 +494,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.) var mousePos = mouseScreenPos; if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0) - ClientToScreen(focusedWindow, &mousePos); + { + // Use game window, otherwise, positions are calculated based on the focused window which might not be the game. + // Leads to offsets. + ClientToScreen(this.hWnd, &mousePos); + } + io.AddMousePosEvent(mousePos.x, mousePos.y); } From a39763f161b893e5735b0dbe3bd7dde42bb38d5f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 10 Dec 2025 18:32:18 +0100 Subject: [PATCH 303/477] Mark preset dirty when disabling clickthrough for a window --- Dalamud/Interface/Windowing/Window.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index ed9318e49..5a79a017a 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -84,7 +84,7 @@ public abstract class Window Click = _ => { this.internalIsClickthrough = false; - this.presetDirty = false; + this.presetDirty = true; ImGui.OpenPopup(AdditionsPopupName); }, Priority = int.MinValue, @@ -905,7 +905,7 @@ public abstract class Window private void DrawErrorMessage() { // TODO: Once window systems are services, offer to reload the plugin - ImGui.TextColoredWrapped(ImGuiColors.DalamudRed,Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); ImGuiHelpers.ScaledDummy(5); From 0b55dc3e10b1a43013fee7893ed3dd88762a2f60 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 11 Dec 2025 22:59:50 +0100 Subject: [PATCH 304/477] Clear ImDrawListSplitter when disposing SeStringDrawState --- .../ImGuiSeStringRenderer/Internal/SeStringRenderer.cs | 2 +- .../Interface/ImGuiSeStringRenderer/SeStringDrawState.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index 397502b30..4a8e6517e 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -170,7 +170,7 @@ internal class SeStringRenderer : IServiceType // This also does argument validation for drawParams. Do it here. // `using var` makes a struct read-only, but we do want to modify it. - var stateStorage = new SeStringDrawState( + using var stateStorage = new SeStringDrawState( sss, drawParams, ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes), diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 5601100e9..885508bed 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -10,6 +10,7 @@ using Dalamud.Interface.Utility; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; + using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; @@ -17,7 +18,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer; /// Calculated values from using ImGui styles. [StructLayout(LayoutKind.Sequential)] -public unsafe ref struct SeStringDrawState +public unsafe ref struct SeStringDrawState : IDisposable { private static readonly int ChannelCount = Enum.GetValues().Length; @@ -194,6 +195,9 @@ public unsafe ref struct SeStringDrawState /// Gets the text fragments. internal List Fragments { get; } + /// + public void Dispose() => this.splitter.ClearFreeMemory(); + /// Sets the current channel in the ImGui draw list splitter. /// Channel to switch to. [MethodImpl(MethodImplOptions.AggressiveInlining)] From e100ec2abdc490838f3422bf5fd5cda695a886fd Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 12 Dec 2025 00:57:04 +0100 Subject: [PATCH 305/477] build: 13.0.0.16 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 5aca47e0c..3ec3c0865 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.15 + 13.0.0.16 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 2d096d9b334ee485764a1e00589cba65c273af3e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 13 Dec 2025 05:05:03 +0100 Subject: [PATCH 306/477] Properly initialize GameInventoryItems (#2504) --- Dalamud/Game/Inventory/GameInventory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 535b84372..5390c2707 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -305,7 +305,8 @@ internal class GameInventory : IInternalDisposableService private GameInventoryItem[] CreateItemsArray(int length) { var items = new GameInventoryItem[length]; - items.Initialize(); + foreach (ref var item in items.AsSpan()) + item = new(); return items; } From a1409096fdc0d882a659f1f84c83fa42646da81d Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 15 Dec 2025 21:20:24 +0100 Subject: [PATCH 307/477] Redo SeStringRenderer deprecations --- .../Internal/SeStringRenderer.cs | 3 +-- .../ImGuiSeStringRenderer/SeStringDrawState.cs | 15 ++++----------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index 4a8e6517e..f161c1868 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -162,8 +162,7 @@ internal class SeStringRenderer : IServiceType if (drawParams.Font.HasValue) font = drawParams.Font.Value; - // API14: Remove commented out code - if (ThreadSafety.IsMainThread /* && drawParams.TargetDrawList is null */ && font is null) + if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null) font = ImGui.GetFont(); if (font is null) throw new ArgumentException("Specified font is empty."); diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 5edf60e9d..dcbe123e7 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -66,17 +66,10 @@ public unsafe ref struct SeStringDrawState : IDisposable this.drawList = ssdp.TargetDrawList.Value; this.ScreenOffset = ssdp.ScreenOffset ?? Vector2.Zero; - // API14: Remove, always throw - if (ThreadSafety.IsMainThread) - { - this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos(); - this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); - } - else - { - throw new ArgumentException( - $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); - } + this.ScreenOffset = ssdp.ScreenOffset ?? throw new ArgumentException( + $"{nameof(ssdp.ScreenOffset)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state. (GetCursorScreenPos?)"); + this.FontSize = ssdp.FontSize ?? throw new ArgumentException( + $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); // this.FontSize = ssdp.FontSize ?? throw new ArgumentException( // $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); From 20af5b40c74d882a7707906e1be034f3f76ff8d2 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 15 Dec 2025 21:31:25 +0100 Subject: [PATCH 308/477] Make all versioning functions internal, move to separate class --- Dalamud/EntryPoint.cs | 4 +- Dalamud/Game/ChatHandlers.cs | 6 +- Dalamud/Interface/Internal/DalamudCommands.cs | 4 +- .../Interface/Internal/DalamudInterface.cs | 8 +- .../Internal/Windows/BranchSwitcherWindow.cs | 2 +- .../Internal/Windows/ChangelogWindow.cs | 4 +- .../Data/Widgets/SeStringCreatorWidget.cs | 2 +- .../PluginInstaller/PluginInstallerWindow.cs | 8 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 2 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 2 +- Dalamud/Networking/Http/HappyHttpClient.cs | 2 +- .../Rpc/Service/ClientHelloService.cs | 2 +- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- .../Plugin/Internal/Types/PluginRepository.cs | 8 +- Dalamud/Support/BugBait.cs | 2 +- Dalamud/Support/DalamudReleases.cs | 2 +- Dalamud/Support/Troubleshooting.cs | 6 +- Dalamud/Utility/Util.cs | 96 ---------------- Dalamud/Utility/Versioning.cs | 108 ++++++++++++++++++ 20 files changed, 142 insertions(+), 130 deletions(-) create mode 100644 Dalamud/Utility/Versioning.cs diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 54e25b6f2..d9f6ef172 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -192,8 +192,8 @@ public sealed class EntryPoint var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", - Util.GetScmVersion(), - Util.GetGitHashClientStructs(), + Versioning.GetScmVersion(), + Versioning.GetGitHashClientStructs(), FFXIVClientStructs.ThisAssembly.Git.Commits); dalamud.WaitForUnload(); diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index b1b798a8a..279bf46e5 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -104,7 +104,7 @@ internal partial class ChatHandlers : IServiceType if (this.configuration.PrintDalamudWelcomeMsg) { - chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion()) + chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Versioning.GetScmVersion()) + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded))); } @@ -116,7 +116,7 @@ internal partial class ChatHandlers : IServiceType } } - if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion)) + if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Versioning.GetAssemblyVersion().StartsWith(this.configuration.LastVersion)) { var linkPayload = chatGui.AddChatLinkHandler( (_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs)); @@ -137,7 +137,7 @@ internal partial class ChatHandlers : IServiceType Type = XivChatType.Notice, }); - this.configuration.LastVersion = Util.AssemblyVersion; + this.configuration.LastVersion = Versioning.GetAssemblyVersion(); this.configuration.QueueSave(); } diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index b1fdb5232..3e4a5cec6 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -305,12 +305,12 @@ internal class DalamudCommands : IServiceType chatGui.Print(new SeStringBuilder() .AddItalics("Dalamud:") - .AddText($" {Util.GetScmVersion()}") + .AddText($" {Versioning.GetScmVersion()}") .Build()); chatGui.Print(new SeStringBuilder() .AddItalics("FFXIVCS:") - .AddText($" {Util.GetGitHashClientStructs()}") + .AddText($" {Versioning.GetGitHashClientStructs()}") .Build()); } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index b0fbeb6c5..be4228a81 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -182,7 +182,7 @@ internal class DalamudInterface : IInternalDisposableService () => Service.GetNullable()?.ToggleDevMenu(), VirtualKey.SHIFT); - if (Util.GetActiveTrack() != "release") + if (Versioning.GetActiveTrack() != "release") { titleScreenMenu.AddEntryCore( Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), @@ -865,7 +865,7 @@ internal class DalamudInterface : IInternalDisposableService } ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false, false); - ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false, false); + ImGui.MenuItem($"D: {Versioning.GetScmVersion()} CS: {Versioning.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false, false); ImGui.MenuItem($"CLR: {Environment.Version}", false, false); ImGui.EndMenu(); @@ -1076,8 +1076,8 @@ internal class DalamudInterface : IInternalDisposableService { ImGui.PushFont(InterfaceManager.MonoFont); - ImGui.BeginMenu($"{Util.GetActiveTrack() ?? "???"} on {Util.GetGitBranch() ?? "???"}", false); - ImGui.BeginMenu($"{Util.GetScmVersion()}", false); + ImGui.BeginMenu($"{Versioning.GetActiveTrack() ?? "???"} on {Versioning.GetGitBranch() ?? "???"}", false); + ImGui.BeginMenu($"{Versioning.GetScmVersion()}", false); ImGui.BeginMenu(this.FrameCount.ToString("000000"), false); ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false); diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index 9cc14ea14..51a9c48a6 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -47,7 +47,7 @@ public class BranchSwitcherWindow : Window this.branches = await client.GetFromJsonAsync>(BranchInfoUrl); Debug.Assert(this.branches != null, "this.branches != null"); - var trackName = Util.GetActiveTrack(); + var trackName = Versioning.GetActiveTrack(); this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track == trackName); if (this.selectedBranchIndex == -1) { diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index b0a910ead..44626ba31 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -147,7 +147,7 @@ internal sealed class ChangelogWindow : Window, IDisposable var pmWantsChangelog = pm?.InstalledPlugins.Any() ?? true; return (string.IsNullOrEmpty(configuration.LastChangelogMajorMinor) || (!WarrantsChangelogForMajorMinor.StartsWith(configuration.LastChangelogMajorMinor) && - Util.AssemblyVersion.StartsWith(WarrantsChangelogForMajorMinor))) && pmWantsChangelog; + Versioning.GetAssemblyVersion().StartsWith(WarrantsChangelogForMajorMinor))) && pmWantsChangelog; } /// @@ -357,7 +357,7 @@ internal sealed class ChangelogWindow : Window, IDisposable { case State.WindowFadeIn: case State.ExplainerIntro: - ImGui.TextWrapped($"Welcome to Dalamud v{Util.GetScmVersion()}!"); + ImGui.TextWrapped($"Welcome to Dalamud v{Versioning.GetScmVersion()}!"); ImGuiHelpers.ScaledDummy(5); ImGui.TextWrapped(ChangeLog); ImGuiHelpers.ScaledDummy(5); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index a88f576f9..e9b4022e4 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -144,7 +144,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget new TextEntry(TextEntryType.Macro, " "), ]; - private SeStringParameter[]? localParameters = [Util.GetScmVersion()]; + private SeStringParameter[]? localParameters = [Versioning.GetScmVersion()]; private ReadOnlySeString input; private ClientLanguage? language; private Task? validImportSheetNamesTask; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index ac092bd25..3241015fc 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -302,7 +302,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.profileManagerWidget.Reset(); - if (this.staleDalamudNewVersion == null && !Util.GetActiveTrack().IsNullOrEmpty()) + if (this.staleDalamudNewVersion == null && !Versioning.GetActiveTrack().IsNullOrEmpty()) { Service.Get().GetVersionForCurrentTrack().ContinueWith(t => { @@ -310,7 +310,7 @@ internal class PluginInstallerWindow : Window, IDisposable return; var versionInfo = t.Result; - if (versionInfo.AssemblyVersion != Util.GetScmVersion()) + if (versionInfo.AssemblyVersion != Versioning.GetScmVersion()) { this.staleDalamudNewVersion = versionInfo.AssemblyVersion; } @@ -1670,7 +1670,7 @@ internal class PluginInstallerWindow : Window, IDisposable DrawWarningIcon(); DrawLinesCentered("A new version of Dalamud is available.\n" + "Please restart the game to ensure compatibility with updated plugins.\n" + - $"old: {Util.GetScmVersion()} new: {this.staleDalamudNewVersion}"); + $"old: {Versioning.GetScmVersion()} new: {this.staleDalamudNewVersion}"); ImGuiHelpers.ScaledDummy(10); } @@ -2461,7 +2461,7 @@ internal class PluginInstallerWindow : Window, IDisposable var isOutdated = effectiveApiLevel < PluginManager.DalamudApiLevel; var isIncompatible = manifest.MinimumDalamudVersion != null && - manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed; + manifest.MinimumDalamudVersion > Versioning.GetAssemblyVersionParsed(); var enableInstallButton = this.updateStatus != OperationStatus.InProgress && this.installStatus != OperationStatus.InProgress && diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 74b9b0fd7..4785ceb3c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -223,7 +223,7 @@ Contribute at: https://github.com/goatcorp/Dalamud .Select(plugin => $"{plugin.Manifest.Name} by {plugin.Manifest.Author}\n") .Aggregate(string.Empty, (current, next) => $"{current}{next}"); - this.creditsText = string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits, Util.GetGitHashClientStructs()); + this.creditsText = string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits, Versioning.GetGitHashClientStructs()); var gameGui = Service.Get(); var playerState = PlayerState.Instance(); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 69cdc4d28..e14dbc545 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -503,7 +503,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable lssb.PushEdgeColorType(701).PushColorType(539) .Append(SeIconChar.BoxedLetterD.ToIconChar()) .PopColorType().PopEdgeColorType(); - lssb.Append($" Dalamud: {Util.GetScmVersion()}"); + lssb.Append($" Dalamud: {Versioning.GetScmVersion()}"); lssb.Append($" - {count} {(count != 1 ? "plugins" : "plugin")} loaded"); diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index aeed98695..c6a476fff 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -36,7 +36,7 @@ internal class HappyHttpClient : IInternalDisposableService { UserAgent = { - new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + new ProductInfoHeaderValue("Dalamud", Versioning.GetAssemblyVersion()), }, }, }; diff --git a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs index c5a4c851a..ae8319f21 100644 --- a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs +++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs @@ -38,7 +38,7 @@ internal sealed class ClientHelloService : IInternalDisposableService return new ClientHelloResponse { ApiVersion = "1.0", - DalamudVersion = Util.GetScmVersion(), + DalamudVersion = Versioning.GetScmVersion(), GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", ProcessId = Environment.ProcessId, ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(), diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index e2eded57c..193a2d45f 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1790,7 +1790,7 @@ internal class PluginManager : IInternalDisposableService var updates = this.AvailablePlugins .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) .Where(remoteManifest => plugin.Manifest.InstalledFromUrl == remoteManifest.SourceRepo.PluginMasterUrl || !remoteManifest.SourceRepo.IsThirdParty) - .Where(remoteManifest => remoteManifest.MinimumDalamudVersion == null || Util.AssemblyVersionParsed >= remoteManifest.MinimumDalamudVersion) + .Where(remoteManifest => remoteManifest.MinimumDalamudVersion == null || Versioning.GetAssemblyVersionParsed() >= remoteManifest.MinimumDalamudVersion) .Where(remoteManifest => { var useTesting = this.UseTesting(remoteManifest); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 0197683ef..1fe18b95b 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -315,7 +315,7 @@ internal class LocalPlugin : IAsyncDisposable if (!this.CheckPolicy()) throw new PluginPreconditionFailedException($"Unable to load {this.Name} as a load policy forbids it"); - if (this.Manifest.MinimumDalamudVersion != null && this.Manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed) + if (this.Manifest.MinimumDalamudVersion != null && this.Manifest.MinimumDalamudVersion > Versioning.GetAssemblyVersionParsed()) throw new PluginPreconditionFailedException($"Unable to load {this.Name}, Dalamud version is lower than minimum required version {this.Manifest.MinimumDalamudVersion}"); this.State = PluginState.Loading; diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index c5e703e4b..d5c1131af 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -59,7 +59,7 @@ internal class PluginRepository }, UserAgent = { - new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + new ProductInfoHeaderValue("Dalamud", Versioning.GetAssemblyVersion()), }, }, }; @@ -164,7 +164,7 @@ internal class PluginRepository } this.PluginMaster = pluginMaster.Where(this.IsValidManifest).ToList().AsReadOnly(); - + // API9 HACK: Force IsHide to false, we should remove that if (!this.IsThirdParty) { @@ -197,7 +197,7 @@ internal class PluginRepository Log.Error("Plugin {PluginName} in {RepoLink} has an invalid Name.", manifest.InternalName, this.PluginMasterUrl); return false; } - + // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (manifest.AssemblyVersion == null) { @@ -224,7 +224,7 @@ internal class PluginRepository request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; using var requestCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); - + return await httpClient.SendAsync(request, requestCts.Token); } } diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index 7ce96208c..f0a98ca98 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -37,7 +37,7 @@ internal static class BugBait Name = plugin.InternalName, Version = isTesting ? plugin.TestingAssemblyVersion?.ToString() : plugin.AssemblyVersion.ToString(), Platform = Util.GetHostPlatform().ToString(), - DalamudHash = Util.GetScmVersion(), + DalamudHash = Versioning.GetScmVersion(), }; if (includeException) diff --git a/Dalamud/Support/DalamudReleases.cs b/Dalamud/Support/DalamudReleases.cs index 603c77487..949ebf94a 100644 --- a/Dalamud/Support/DalamudReleases.cs +++ b/Dalamud/Support/DalamudReleases.cs @@ -38,7 +38,7 @@ internal class DalamudReleases : IServiceType /// The version info for the current track. public async Task GetVersionForCurrentTrack() { - var currentTrack = Util.GetActiveTrack(); + var currentTrack = Versioning.GetActiveTrack(); if (currentTrack.IsNullOrEmpty()) return null; diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index 88048c462..de529a29b 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -69,11 +69,11 @@ public static class Troubleshooting LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(), PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()), EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(), - DalamudVersion = Util.GetScmVersion(), - DalamudGitHash = Util.GetGitHash() ?? "Unknown", + DalamudVersion = Versioning.GetScmVersion(), + DalamudGitHash = Versioning.GetGitHash() ?? "Unknown", GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown", Language = startInfo.Language.ToString(), - BetaKey = Util.GetActiveTrack(), + BetaKey = Versioning.GetActiveTrack(), DoPluginTest = configuration.DoPluginTest, LoadAllApiLevels = pluginManager?.LoadAllApiLevels == true, InterfaceLoaded = interfaceManager?.IsReady ?? false, diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index bde113904..0ea5bbcbf 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -68,96 +68,10 @@ public static partial class Util ]; private static readonly Type GenericSpanType = typeof(Span<>); - private static string? scmVersionInternal; - private static string? gitHashInternal; - private static string? gitHashClientStructsInternal; - private static string? branchInternal; private static ulong moduleStartAddr; private static ulong moduleEndAddr; - /// - /// Gets the Dalamud version. - /// - [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] - public static string AssemblyVersion { get; } = - Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); - - /// - /// Gets the Dalamud version. - /// - internal static Version AssemblyVersionParsed { get; } = - Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!; - - /// - /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return - /// the git describe output for this build, which will be a raw version if this is a stable build or an - /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string. - /// - /// The SCM version of the assembly. - public static string GetScmVersion() - { - if (scmVersionInternal != null) return scmVersionInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value - ?? asm.GetName().Version!.ToString(); - } - - /// - /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds, - /// and will be suffixed with `-dirty` if in release with pending changes. - /// - /// The git hash of the assembly. - public static string? GetGitHash() - { - if (gitHashInternal != null) - return gitHashInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - return gitHashInternal = attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value ?? "N/A"; - } - - /// - /// Gets the git hash value from the assembly or null if it cannot be found. - /// - /// The git hash of the assembly. - public static string? GetGitHashClientStructs() - { - if (gitHashClientStructsInternal != null) - return gitHashClientStructsInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - gitHashClientStructsInternal = attrs.First(a => a.Key == "GitHashClientStructs").Value; - - return gitHashClientStructsInternal; - } - - /// - /// Gets the Git branch name this version of Dalamud was built from, or null, if this is a Debug build. - /// - /// The branch name. - public static string? GetGitBranch() - { - if (branchInternal != null) - return branchInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - var gitBranch = attrs.FirstOrDefault(a => a.Key == "GitBranch")?.Value; - if (gitBranch == null) - return null; - - return branchInternal = gitBranch; - } - /// public static unsafe string DescribeAddress(void* p) => DescribeAddress((nint)p); @@ -693,16 +607,6 @@ public static partial class Util } } - /// - /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version - /// downloaded from webservices. - /// - /// The name of the track, or null. - internal static string? GetActiveTrack() - { - return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); - } - /// /// Gets a random, inoffensive, human-friendly string. /// diff --git a/Dalamud/Utility/Versioning.cs b/Dalamud/Utility/Versioning.cs new file mode 100644 index 000000000..d3b30b834 --- /dev/null +++ b/Dalamud/Utility/Versioning.cs @@ -0,0 +1,108 @@ +using System.Linq; +using System.Reflection; + +namespace Dalamud.Utility; + +/// +/// Helpers to access Dalamud versioning information. +/// +internal static class Versioning +{ + private static string? scmVersionInternal; + private static string? gitHashInternal; + private static string? gitHashClientStructsInternal; + private static string? branchInternal; + + /// + /// Gets the Dalamud version. + /// + /// The raw Dalamud assembly version. + internal static string GetAssemblyVersion() => + Assembly.GetAssembly(typeof(Versioning))!.GetName().Version!.ToString(); + + /// + /// Gets the Dalamud version. + /// + /// The parsed Dalamud assembly version. + internal static Version GetAssemblyVersionParsed() => + Assembly.GetAssembly(typeof(Versioning))!.GetName().Version!; + + /// + /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return + /// the git describe output for this build, which will be a raw version if this is a stable build or an + /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string. + /// + /// The SCM version of the assembly. + internal static string GetScmVersion() + { + if (scmVersionInternal != null) return scmVersionInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value + ?? asm.GetName().Version!.ToString(); + } + + /// + /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds, + /// and will be suffixed with `-dirty` if in release with pending changes. + /// + /// The git hash of the assembly. + internal static string? GetGitHash() + { + if (gitHashInternal != null) + return gitHashInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + return gitHashInternal = attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value ?? "N/A"; + } + + /// + /// Gets the git hash value from the assembly or null if it cannot be found. + /// + /// The git hash of the assembly. + internal static string? GetGitHashClientStructs() + { + if (gitHashClientStructsInternal != null) + return gitHashClientStructsInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + gitHashClientStructsInternal = attrs.First(a => a.Key == "GitHashClientStructs").Value; + + return gitHashClientStructsInternal; + } + + /// + /// Gets the Git branch name this version of Dalamud was built from, or null, if this is a Debug build. + /// + /// The branch name. + internal static string? GetGitBranch() + { + if (branchInternal != null) + return branchInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + var gitBranch = attrs.FirstOrDefault(a => a.Key == "GitBranch")?.Value; + if (gitBranch == null) + return null; + + return branchInternal = gitBranch; + } + + /// + /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version + /// downloaded from webservices. + /// + /// The name of the track, or null. + internal static string? GetActiveTrack() + { + return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); + } +} From ffd99d57914a9306c47c146257801b607ed2d9b3 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 15 Dec 2025 21:43:52 +0100 Subject: [PATCH 309/477] Add interface to obtain versioning info --- Dalamud/Plugin/DalamudPluginInterface.cs | 25 ++++++++----------- Dalamud/Plugin/IDalamudPluginInterface.cs | 8 +++++- .../Plugin/VersionInfo/DalamudVersionInfo.cs | 11 ++++++++ .../Plugin/VersionInfo/IDalamudVersionInfo.cs | 19 ++++++++++++++ 4 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs create mode 100644 Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 6fd9064b6..1051f908c 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -16,18 +16,15 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.Sanitizer; using Dalamud.Interface; using Dalamud.Interface.Internal; -using Dalamud.Interface.Internal.Windows.PluginInstaller; -using Dalamud.Interface.Internal.Windows.SelfTest; -using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; -using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.VersionInfo; +using Dalamud.Utility; using Serilog; @@ -204,11 +201,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa return true; } - /// - /// Gets the plugin the given assembly is part of. - /// - /// The assembly to check. - /// The plugin the given assembly is part of, or null if this is a shared assembly or if this information cannot be determined. + /// public IExposedPlugin? GetPlugin(Assembly assembly) => AssemblyLoadContext.GetLoadContext(assembly) switch { @@ -216,11 +209,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa var context => this.GetPlugin(context), }; - /// - /// Gets the plugin that loads in the given context. - /// - /// The context to check. - /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. + /// public IExposedPlugin? GetPlugin(AssemblyLoadContext context) => Service.Get().InstalledPlugins.FirstOrDefault(p => p.LoadsIn(context)) switch { @@ -228,6 +217,12 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa var p => new ExposedPlugin(p), }; + /// + public IDalamudVersionInfo GetDalamudVersion() + { + return new DalamudVersionInfo(Versioning.GetAssemblyVersionParsed(), Versioning.GetActiveTrack()); + } + #region IPC /// diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index d1b6977d4..92ecab006 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -15,7 +15,7 @@ using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.VersionInfo; namespace Dalamud.Plugin; @@ -194,6 +194,12 @@ public interface IDalamudPluginInterface : IServiceProvider /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. IExposedPlugin? GetPlugin(AssemblyLoadContext context); + /// + /// Gets information about the version of Dalamud this plugin is loaded into. + /// + /// Class containing version information. + IDalamudVersionInfo GetDalamudVersion(); + /// T GetOrCreateData(string tag, Func dataGenerator) where T : class; diff --git a/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs new file mode 100644 index 000000000..c87c012af --- /dev/null +++ b/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs @@ -0,0 +1,11 @@ +namespace Dalamud.Plugin.VersionInfo; + +/// +internal class DalamudVersionInfo(Version version, string? track) : IDalamudVersionInfo +{ + /// + public Version Version { get; } = version; + + /// + public string? BetaTrack { get; } = track; +} diff --git a/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs new file mode 100644 index 000000000..e6b6a9601 --- /dev/null +++ b/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Plugin.VersionInfo; + +/// +/// Interface exposing various information related to Dalamud versioning. +/// +public interface IDalamudVersionInfo +{ + /// + /// Gets the Dalamud version. + /// + Version Version { get; } + + /// + /// Gets the currently used beta track. + /// Please don't tell users to switch branches. They have it bad enough, fix your things instead. + /// Null if this build wasn't launched from XIVLauncher. + /// + string? BetaTrack { get; } +} From a715725a9d8475f5b6755d70724e5ca47d753254 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Mon, 15 Dec 2025 13:13:08 -0800 Subject: [PATCH 310/477] Add enumerable AtkValue helper --- .../AddonArgTypes/AddonRefreshArgs.cs | 30 +++++++++++++++++-- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 30 +++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index 8af017318..cb9de8088 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; + +using Dalamud.Game.NativeWrapper; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -32,7 +35,30 @@ public class AddonRefreshArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// - [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] - [Api15ToDo("Remove this")] + [Obsolete("Pending removal, Use AtkValueEnumerable instead.")] + [Api15ToDo("Make this internal, remove obsolete")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + /// Gets an enumerable collection of of the event's AtkValues. + /// + /// + /// An of corresponding to the event's AtkValues. + /// + public IEnumerable AtkValueEnumerable + { + get + { + for (var i = 0; i < this.AtkValueCount; i++) + { + AtkValuePtr ptr; + unsafe + { + ptr = new AtkValuePtr((nint)this.AtkValueSpan[i].Pointer); + } + + yield return ptr; + } + } + } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 9fd7b6dd0..2501d159f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; + +using Dalamud.Game.NativeWrapper; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -32,7 +35,30 @@ public class AddonSetupArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// - [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] - [Api15ToDo("Remove this")] + [Obsolete("Pending removal, Use AtkValueEnumerable instead.")] + [Api15ToDo("Make this internal, remove obsolete")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + /// Gets an enumerable collection of of the event's AtkValues. + /// + /// + /// An of corresponding to the event's AtkValues. + /// + public IEnumerable AtkValueEnumerable + { + get + { + for (var i = 0; i < this.AtkValueCount; i++) + { + AtkValuePtr ptr; + unsafe + { + ptr = new AtkValuePtr((nint)this.AtkValueSpan[i].Pointer); + } + + yield return ptr; + } + } + } } From 1bff6abae90eb86b48a4465acc7de5cb289dbafa Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Mon, 15 Dec 2025 13:22:39 -0800 Subject: [PATCH 311/477] Fix oopsie --- Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs | 3 ++- Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index cb9de8088..4fc81632a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -4,6 +4,7 @@ using Dalamud.Game.NativeWrapper; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; +using FFXIVClientStructs.Interop; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -54,7 +55,7 @@ public class AddonRefreshArgs : AddonArgs AtkValuePtr ptr; unsafe { - ptr = new AtkValuePtr((nint)this.AtkValueSpan[i].Pointer); + ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); } yield return ptr; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 2501d159f..e0b2defbf 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -4,6 +4,7 @@ using Dalamud.Game.NativeWrapper; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; +using FFXIVClientStructs.Interop; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -54,7 +55,7 @@ public class AddonSetupArgs : AddonArgs AtkValuePtr ptr; unsafe { - ptr = new AtkValuePtr((nint)this.AtkValueSpan[i].Pointer); + ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); } yield return ptr; From 56325afa7fa6f897f61a93396bb78714600cad1e Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:14:46 +0100 Subject: [PATCH 312/477] Remove obsolete enum values from FlyTextKind They have been obsolete for nearly 7 months (before 7.3). --- Dalamud/Game/Gui/FlyText/FlyTextKind.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs index 2b8325927..da448c683 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs @@ -92,34 +92,16 @@ public enum FlyTextKind : int /// IslandExp = 15, - /// - /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. - /// - [Obsolete("Use Dataset instead", true)] - Unknown16 = 16, - /// /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. /// Dataset = 16, - /// - /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// - [Obsolete("Use Knowledge instead", true)] - Unknown17 = 17, - /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// Knowledge = 17, - /// - /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// - [Obsolete("Use PhantomExp instead", true)] - Unknown18 = 18, - /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// From 1c1b60efeee642577196a6a319cbc0539895819b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 16 Dec 2025 09:48:59 +0100 Subject: [PATCH 313/477] Better enumerator code --- .../Game/ClientState/Aetherytes/AetheryteList.cs | 16 ++++++++++------ Dalamud/Game/ClientState/Buddy/BuddyList.cs | 16 ++++++++++------ Dalamud/Game/ClientState/Fates/FateTable.cs | 16 ++++++++++------ Dalamud/Game/ClientState/Objects/ObjectTable.cs | 14 ++++++++------ Dalamud/Game/ClientState/Party/PartyList.cs | 9 ++++----- Dalamud/Game/ClientState/Statuses/StatusList.cs | 9 ++++----- 6 files changed, 46 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index a24302947..12a629958 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -99,7 +99,7 @@ internal sealed partial class AetheryteList private struct Enumerator(AetheryteList aetheryteList) : IEnumerator { - private int index = 0; + private int index = -1; public IAetheryteEntry Current { get; private set; } @@ -107,15 +107,19 @@ internal sealed partial class AetheryteList public bool MoveNext() { - if (this.index == aetheryteList.Length) return false; - this.Current = aetheryteList[this.index]; - this.index++; - return true; + if (++this.index < aetheryteList.Length) + { + this.Current = aetheryteList[this.index]; + return true; + } + + this.Current = default; + return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index b8e4c0fcc..3bec6d9f1 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -141,7 +141,7 @@ internal sealed partial class BuddyList private struct Enumerator(BuddyList buddyList) : IEnumerator { - private int index = 0; + private int index = -1; public IBuddyMember Current { get; private set; } @@ -149,15 +149,19 @@ internal sealed partial class BuddyList public bool MoveNext() { - if (this.index == buddyList.Length) return false; - this.Current = buddyList[this.index]; - this.index++; - return true; + if (++this.index < buddyList.Length) + { + this.Current = buddyList[this.index]; + return true; + } + + this.Current = default; + return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index fa75c7e53..41e974f04 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -115,7 +115,7 @@ internal sealed partial class FateTable private struct Enumerator(FateTable fateTable) : IEnumerator { - private int index = 0; + private int index = -1; public IFate Current { get; private set; } @@ -123,15 +123,19 @@ internal sealed partial class FateTable public bool MoveNext() { - if (this.index == fateTable.Length) return false; - this.Current = fateTable[this.index]; - this.index++; - return true; + if (++this.index < fateTable.Length) + { + this.Current = fateTable[this.index]; + return true; + } + + this.Current = default; + return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 6bbc43235..9a2c7343e 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -246,17 +246,15 @@ internal sealed partial class ObjectTable { private int index = -1; - public IGameObject Current { get; private set; } = null!; + public IGameObject Current { get; private set; } object IEnumerator.Current => this.Current; public bool MoveNext() { - if (this.index == objectTableLength) - return false; - var cache = owner.cachedObjectTable.AsSpan(); - for (this.index++; this.index < objectTableLength; this.index++) + + while (++this.index < objectTableLength) { if (cache[this.index].Update() is { } ao) { @@ -265,10 +263,14 @@ internal sealed partial class ObjectTable } } + this.Current = default; return false; } - public void Reset() => this.index = -1; + public void Reset() + { + this.index = -1; + } public void Dispose() { diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 1dede1dd3..90959f926 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -143,7 +143,7 @@ internal sealed partial class PartyList private struct Enumerator(PartyList partyList) : IEnumerator { - private int index = 0; + private int index = -1; public IPartyMember Current { get; private set; } @@ -151,9 +151,7 @@ internal sealed partial class PartyList public bool MoveNext() { - if (this.index == partyList.Length) return false; - - for (; this.index < partyList.Length; this.index++) + while (++this.index < partyList.Length) { var partyMember = partyList[this.index]; if (partyMember != null) @@ -163,12 +161,13 @@ internal sealed partial class PartyList } } + this.Current = default; return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index 81469ba93..43650a48c 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -153,7 +153,7 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollecti private struct Enumerator(StatusList statusList) : IEnumerator { - private int index = 0; + private int index = -1; public IStatus Current { get; private set; } @@ -161,9 +161,7 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollecti public bool MoveNext() { - if (this.index == statusList.Length) return false; - - for (; this.index < statusList.Length; this.index++) + while (++this.index < statusList.Length) { var status = statusList[this.index]; if (status != null && status.StatusId != 0) @@ -173,12 +171,13 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollecti } } + this.Current = default; return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() From 89fbe6c8b098af79f0b451c07b9b0c611a682ee5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 16 Dec 2025 17:21:19 +0100 Subject: [PATCH 314/477] Update UiConfigOption --- Dalamud/Game/Config/UiConfigOption.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Dalamud/Game/Config/UiConfigOption.cs b/Dalamud/Game/Config/UiConfigOption.cs index f6a9aaa21..0cfc3b1e9 100644 --- a/Dalamud/Game/Config/UiConfigOption.cs +++ b/Dalamud/Game/Config/UiConfigOption.cs @@ -4069,6 +4069,13 @@ public enum UiConfigOption [GameConfigOption("GposePortraitRotateType", ConfigType.UInt)] GposePortraitRotateType, + /// + /// UiConfig option with the internal name GroupPosePortraitUnlockAspectLimit. + /// This option is a UInt. + /// + [GameConfigOption("GroupPosePortraitUnlockAspectLimit", ConfigType.UInt)] + GroupPosePortraitUnlockAspectLimit, + /// /// UiConfig option with the internal name LsListSortPriority. /// This option is a UInt. From cdf4e2735504040df09be5a27b5fc984349e9c40 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 16 Dec 2025 19:28:09 +0100 Subject: [PATCH 315/477] Bump version to 14.0.0.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 869faf2da..9685b92ac 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.16 + 14.0.0.0 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 01901c237a178fb00bbf271ea684fc058df49109 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 16 Dec 2025 21:01:50 +0100 Subject: [PATCH 316/477] Downgrade Iced to resolve version conflict between Dalamud and Injector --- Directory.Packages.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ec2e7e276..06338efac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,7 +33,8 @@ - + + From 46954e6add867b1eb5b86a46c1ce647084b664d7 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 16 Dec 2025 21:01:58 +0100 Subject: [PATCH 317/477] Remove plugin targets from SLN --- Dalamud.sln | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dalamud.sln b/Dalamud.sln index ee3c75b25..de91e7ceb 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig .gitignore = .gitignore tools\BannedSymbols.txt = tools\BannedSymbols.txt - targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets - targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets tools\dalamud.ruleset = tools\dalamud.ruleset Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props From f142fb1058887be55cb3b9cbebd370643f4bc3d5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 17 Dec 2025 00:50:14 +0100 Subject: [PATCH 318/477] Set language version to preview for now Fixes a docfx error, since they haven't upgraded to a Roslyn version that knows C# 14 --- Directory.Build.props | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 3897256bf..8a8df22d7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,9 @@ net10.0-windows x64 x64 - 14.0 + + + preview From 19660a20d94dd3af3693cebbae82bbdf477ed6c9 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 16 Dec 2025 21:33:42 +0100 Subject: [PATCH 319/477] Update Condition/ConditionFlag --- Dalamud/Game/ClientState/Conditions/Condition.cs | 2 +- .../Game/ClientState/Conditions/ConditionFlag.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 99748f71b..6f61ab246 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -18,7 +18,7 @@ internal sealed class Condition : IInternalDisposableService, ICondition /// /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// - internal const int MaxConditionEntries = 104; + internal const int MaxConditionEntries = 112; [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); diff --git a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs index 19451dd5c..b5894d891 100644 --- a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs +++ b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs @@ -520,4 +520,17 @@ public enum ConditionFlag PilotingMech = 102, // Unknown103 = 103, + + /// + /// Unable to execute command while editing a strategy board. + /// + EditingStrategyBoard = 104, + + // Unknown105 = 105, + // Unknown106 = 106, + // Unknown107 = 107, + // Unknown108 = 108, + // Unknown109 = 109, + // Unknown110 = 110, + // Unknown111 = 111, } From 841cdf52bd86c3d52e8d1a20add48c9543894949 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 17 Dec 2025 16:13:44 +0100 Subject: [PATCH 320/477] Update Lumina and Lumina.Excel --- ...{ItemActionType.cs => ItemActionAction.cs} | 4 +- Dalamud/Game/UnlockState/RecipeData.cs | 58 ++--------------- Dalamud/Game/UnlockState/UnlockState.cs | 64 +++++++++---------- Directory.Packages.props | 2 +- lib/Lumina.Excel | 2 +- 5 files changed, 43 insertions(+), 87 deletions(-) rename Dalamud/Game/UnlockState/{ItemActionType.cs => ItemActionAction.cs} (96%) diff --git a/Dalamud/Game/UnlockState/ItemActionType.cs b/Dalamud/Game/UnlockState/ItemActionAction.cs similarity index 96% rename from Dalamud/Game/UnlockState/ItemActionType.cs rename to Dalamud/Game/UnlockState/ItemActionAction.cs index 8e3d79b84..0e86fcb67 100644 --- a/Dalamud/Game/UnlockState/ItemActionType.cs +++ b/Dalamud/Game/UnlockState/ItemActionAction.cs @@ -3,9 +3,9 @@ using Lumina.Excel.Sheets; namespace Dalamud.Game.UnlockState; /// -/// Enum for . +/// Enum for . /// -internal enum ItemActionType : ushort +internal enum ItemActionAction : ushort { /// /// No item action. diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index c419ba4fd..7fa0d4b8f 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -158,67 +158,23 @@ internal unsafe class RecipeData : IInternalDisposableService { noteBookDivisionIndex++; - // For future Lumina.Excel update, replace with: - // if (!notebookDivisionRow.AllowedCraftTypes[craftType]) - // continue; - - switch (craftTypeRow.RowId) - { - case 0 when !noteBookDivisionRow.CRPCraft: continue; - case 1 when !noteBookDivisionRow.BSMCraft: continue; - case 2 when !noteBookDivisionRow.ARMCraft: continue; - case 3 when !noteBookDivisionRow.GSMCraft: continue; - case 4 when !noteBookDivisionRow.LTWCraft: continue; - case 5 when !noteBookDivisionRow.WVRCraft: continue; - case 6 when !noteBookDivisionRow.ALCCraft: continue; - case 7 when !noteBookDivisionRow.CULCraft: continue; - } + if (!noteBookDivisionRow.AllowedCraftTypes[craftType]) + continue; if (noteBookDivisionRow.GatheringOpeningLevel != byte.MaxValue) continue; - // For future Lumina.Excel update, replace with: - // if (notebookDivisionRow.RequiresSecretRecipeBookGroupUnlock) - if (noteBookDivisionRow.Unknown1) + if (noteBookDivisionRow.RequiresSecretRecipeBookGroupUnlock) { var secretRecipeBookUnlocked = false; - // For future Lumina.Excel update, iterate over notebookDivisionRow.SecretRecipeBookGroups - for (var i = 0; i < 2; i++) + foreach (var secretRecipeBookGroup in noteBookDivisionRow.SecretRecipeBookGroups) { - // For future Lumina.Excel update, replace with: - // if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid) - // continue; - var secretRecipeBookGroupRowId = i switch - { - 0 => noteBookDivisionRow.Unknown2, - 1 => noteBookDivisionRow.Unknown2, - _ => default, - }; - - if (secretRecipeBookGroupRowId == 0) + if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid) continue; - if (!this.dataManager.GetExcelSheet().TryGetRow(secretRecipeBookGroupRowId, out var secretRecipeBookGroupRow)) - continue; - - // For future Lumina.Excel update, replace with: - // var bitIndex = secretRecipeBookGroup.Value.UnlockBitIndex[craftType]; - - var bitIndex = craftType switch - { - 0 => secretRecipeBookGroupRow.Unknown0, - 1 => secretRecipeBookGroupRow.Unknown1, - 2 => secretRecipeBookGroupRow.Unknown2, - 3 => secretRecipeBookGroupRow.Unknown3, - 4 => secretRecipeBookGroupRow.Unknown4, - 5 => secretRecipeBookGroupRow.Unknown5, - 6 => secretRecipeBookGroupRow.Unknown6, - 7 => secretRecipeBookGroupRow.Unknown7, - _ => default, - }; - - if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get(bitIndex)) + var bitIndex = secretRecipeBookGroup.Value.SecretRecipeBook[craftType].RowId; + if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get((int)bitIndex)) { secretRecipeBookUnlocked = true; break; diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index cd896ffb6..cc70a524c 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -209,7 +209,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState /// public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) { - return this.IsUnlockLinkUnlocked(row.Unknown26); + return this.IsUnlockLinkUnlocked(row.UnlockLink); } /// @@ -217,7 +217,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState { return this.dataManager.GetExcelSheet().TryGetRow(row.RowId, out var emjVoiceNpcRow) && this.IsEmjVoiceNpcUnlocked(emjVoiceNpcRow) - && QuestManager.IsQuestComplete(row.Unknown1); + && QuestManager.IsQuestComplete(row.UnlockQuest.RowId); } /// @@ -264,47 +264,47 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // To avoid the ExdModule.GetItemRowById call, which can return null if the excel page // is not loaded, we're going to imitate the IsItemActionUnlocked call first: - switch ((ItemActionType)row.ItemAction.Value.Type) + switch ((ItemActionAction)row.ItemAction.Value.Action.RowId) { - case ItemActionType.Companion: + case ItemActionAction.Companion: return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.BuddyEquip: + case ItemActionAction.BuddyEquip: return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.Mount: + case ItemActionAction.Mount: return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.SecretRecipeBook: + case ItemActionAction.SecretRecipeBook: return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.UnlockLink: - case ItemActionType.OccultRecords: + case ItemActionAction.UnlockLink: + case ItemActionAction.OccultRecords: return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.TripleTriadCard when row.AdditionalData.Is(): + case ItemActionAction.TripleTriadCard when row.AdditionalData.Is(): return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId); - case ItemActionType.FolkloreTome: + case ItemActionAction.FolkloreTome: return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.OrchestrionRoll when row.AdditionalData.Is(): + case ItemActionAction.OrchestrionRoll when row.AdditionalData.Is(): return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId); - case ItemActionType.FramersKit: + case ItemActionAction.FramersKit: return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId); - case ItemActionType.Ornament: + case ItemActionAction.Ornament: return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.Glasses: + case ItemActionAction.Glasses: return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId); - case ItemActionType.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: + case ItemActionAction.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: var supportJobId = (byte)row.ItemAction.Value.Data[0]; return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0; - case ItemActionType.CompanySealVouchers: + case ItemActionAction.CompanySealVouchers: return false; } @@ -327,7 +327,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState /// public bool IsMKDLoreUnlocked(MKDLore row) { - return this.IsUnlockLinkUnlocked(row.Unknown2); + return this.IsUnlockLinkUnlocked(row.UnlockLink); } /// @@ -414,20 +414,20 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (row.ItemAction.RowId == 0) return false; - return (ItemActionType)row.ItemAction.Value.Type - is ItemActionType.Companion - or ItemActionType.BuddyEquip - or ItemActionType.Mount - or ItemActionType.SecretRecipeBook - or ItemActionType.UnlockLink - or ItemActionType.TripleTriadCard - or ItemActionType.FolkloreTome - or ItemActionType.OrchestrionRoll - or ItemActionType.FramersKit - or ItemActionType.Ornament - or ItemActionType.Glasses - or ItemActionType.OccultRecords - or ItemActionType.SoulShards; + return (ItemActionAction)row.ItemAction.Value.Action.RowId + is ItemActionAction.Companion + or ItemActionAction.BuddyEquip + or ItemActionAction.Mount + or ItemActionAction.SecretRecipeBook + or ItemActionAction.UnlockLink + or ItemActionAction.TripleTriadCard + or ItemActionAction.FolkloreTome + or ItemActionAction.OrchestrionRoll + or ItemActionAction.FramersKit + or ItemActionAction.Ornament + or ItemActionAction.Glasses + or ItemActionAction.OccultRecords + or ItemActionAction.SoulShards; } /// diff --git a/Directory.Packages.props b/Directory.Packages.props index 06338efac..77a4035a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 5d01489c3..4650ad332 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 5d01489c34f33a3d645f49085d7fc0065a1ac801 +Subproject commit 4650ad332dd22aeff0d1f7ac33845b1c2aca4f8d From b3c4363e0fad6f0c9c651467330607d09df5cb01 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 17 Dec 2025 17:09:18 +0100 Subject: [PATCH 321/477] Fix crashing Context Menu --- Dalamud/Game/Gui/ContextMenu/ContextMenu.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs index 7512f4160..aada374ec 100644 --- a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -31,7 +31,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM private static readonly ModuleLog Log = new("ContextMenu"); private readonly Hook atkModuleVf22OpenAddonByAgentHook; - private readonly Hook addonContextMenuOnMenuSelectedHook; + private readonly Hook addonContextMenuOnMenuSelectedHook; private uint? addonContextSubNameId; @@ -40,7 +40,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM { var raptureAtkModuleVtable = (nint*)RaptureAtkModule.StaticVirtualTablePointer; this.atkModuleVf22OpenAddonByAgentHook = Hook.FromAddress(raptureAtkModuleVtable[22], this.AtkModuleVf22OpenAddonByAgentDetour); - this.addonContextMenuOnMenuSelectedHook = Hook.FromAddress((nint)AddonContextMenu.StaticVirtualTablePointer->OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour); + this.addonContextMenuOnMenuSelectedHook = Hook.FromAddress((nint)AddonContextMenu.StaticVirtualTablePointer->OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour); this.atkModuleVf22OpenAddonByAgentHook.Enable(); this.addonContextMenuOnMenuSelectedHook.Enable(); @@ -48,10 +48,6 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8); - private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3); - - private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2); - /// public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened; @@ -185,7 +181,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM values[0].ChangeType(ValueType.UInt); values[0].UInt = 0; values[1].ChangeType(ValueType.String); - values[1].SetManagedString(name.Encode().NullTerminate()); + values[1].SetManagedString(name.EncodeWithNullTerminator()); values[2].ChangeType(ValueType.Int); values[2].Int = x; values[3].ChangeType(ValueType.Int); @@ -265,7 +261,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM submenuMask |= 1u << i; nameData[i].ChangeType(ValueType.String); - nameData[i].SetManagedString(this.GetPrefixedName(item).Encode().NullTerminate()); + nameData[i].SetManagedString(this.GetPrefixedName(item).EncodeWithNullTerminator()); } for (var i = 0; i < prefixMenuSize; ++i) @@ -295,8 +291,9 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM // 2: UInt = Return Mask (?) // 3: UInt = Submenu Mask // 4: UInt = OpenAtCursorPosition ? 2 : 1 - // 5: UInt = 0 - // 6: UInt = 0 + // 5: UInt = ? + // 6: UInt = ? + // 7: UInt = ? foreach (var item in items) { @@ -312,7 +309,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM } } - this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values); + this.SetupGenericMenu(8, 0, 2, 3, items, ref valueCount, ref values); } private void SetupContextSubMenu(IReadOnlyList items, ref int valueCount, ref AtkValue* values) From 2fc9884aad6217e61909f8b602a4782e7b29fd63 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 17 Dec 2025 17:32:39 +0100 Subject: [PATCH 322/477] Update HoverActionKind --- Dalamud/Game/Gui/HoverActionKind.cs | 61 ++++++++++++++++------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/Dalamud/Game/Gui/HoverActionKind.cs b/Dalamud/Game/Gui/HoverActionKind.cs index ef8fe6400..b786f12ee 100644 --- a/Dalamud/Game/Gui/HoverActionKind.cs +++ b/Dalamud/Game/Gui/HoverActionKind.cs @@ -14,140 +14,145 @@ public enum HoverActionKind /// /// A regular action is hovered. /// - Action = 28, + Action = 29, /// /// A crafting action is hovered. /// - CraftingAction = 29, + CraftingAction = 30, /// /// A general action is hovered. /// - GeneralAction = 30, + GeneralAction = 31, /// /// A companion order type of action is hovered. /// - CompanionOrder = 31, // Game Term: BuddyOrder + CompanionOrder = 32, // Game Term: BuddyOrder /// /// A main command type of action is hovered. /// - MainCommand = 32, + MainCommand = 33, /// /// An extras command type of action is hovered. /// - ExtraCommand = 33, + ExtraCommand = 34, /// /// A companion action is hovered. /// - Companion = 34, + Companion = 35, /// /// A pet order type of action is hovered. /// - PetOrder = 35, + PetOrder = 36, /// /// A trait is hovered. /// - Trait = 36, + Trait = 37, /// /// A buddy action is hovered. /// - BuddyAction = 37, + BuddyAction = 38, /// /// A company action is hovered. /// - CompanyAction = 38, + CompanyAction = 39, /// /// A mount is hovered. /// - Mount = 39, + Mount = 40, /// /// A chocobo race action is hovered. /// - ChocoboRaceAction = 40, + ChocoboRaceAction = 41, /// /// A chocobo race item is hovered. /// - ChocoboRaceItem = 41, + ChocoboRaceItem = 42, /// /// A deep dungeon equipment is hovered. /// - DeepDungeonEquipment = 42, + DeepDungeonEquipment = 43, /// /// A deep dungeon equipment 2 is hovered. /// - DeepDungeonEquipment2 = 43, + DeepDungeonEquipment2 = 44, /// /// A deep dungeon item is hovered. /// - DeepDungeonItem = 44, + DeepDungeonItem = 45, /// /// A quick chat is hovered. /// - QuickChat = 45, + QuickChat = 46, /// /// An action combo route is hovered. /// - ActionComboRoute = 46, + ActionComboRoute = 47, /// /// A pvp trait is hovered. /// - PvPSelectTrait = 47, + PvPSelectTrait = 48, /// /// A squadron action is hovered. /// - BgcArmyAction = 48, + BgcArmyAction = 49, /// /// A perform action is hovered. /// - Perform = 49, + Perform = 50, /// /// A deep dungeon magic stone is hovered. /// - DeepDungeonMagicStone = 50, + DeepDungeonMagicStone = 51, /// /// A deep dungeon demiclone is hovered. /// - DeepDungeonDemiclone = 51, + DeepDungeonDemiclone = 52, /// /// An eureka magia action is hovered. /// - EurekaMagiaAction = 52, + EurekaMagiaAction = 53, /// /// An island sanctuary temporary item is hovered. /// - MYCTemporaryItem = 53, + MYCTemporaryItem = 54, /// /// An ornament is hovered. /// - Ornament = 54, + Ornament = 55, /// /// Glasses are hovered. /// - Glasses = 55, + Glasses = 56, + + /// + /// Phantom Job Trait is hovered. + /// + MKDTrait = 58, } From fc804ba0d05f791e4c6d19d546c077b675b9dd8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Dec 2025 18:40:02 +0000 Subject: [PATCH 323/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index e5dedba42..305c1629e 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f +Subproject commit 305c1629eed0b1cdca5efb102e37de93d592d155 From 02d4081f2ff877be565858c2d4aacac1c5f5e46a Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:24:09 +0100 Subject: [PATCH 324/477] ci: disable rollup for now --- .github/workflows/rollup.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml index 8fe049ad7..f4e013258 100644 --- a/.github/workflows/rollup.yml +++ b/.github/workflows/rollup.yml @@ -1,8 +1,8 @@ name: Rollup changes to next version on: - push: - branches: - - master +# push: +# branches: +# - master workflow_dispatch: jobs: From 574e0d458201f0e0af172b0a8fda590cf9db2b4a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 00:24:11 +0000 Subject: [PATCH 325/477] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 4650ad332..c74841abc 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 4650ad332dd22aeff0d1f7ac33845b1c2aca4f8d +Subproject commit c74841abce0830ead4437ed2f560bceb6235a538 From 05037dccc7c6b899b1a4ab1483e763e789e28dee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 00:24:13 +0000 Subject: [PATCH 326/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 305c1629e..c0a862043 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 305c1629eed0b1cdca5efb102e37de93d592d155 +Subproject commit c0a8620439e647ccf443710e81acce021b299bf5 From 25dba5e23b9217b7d8f86af1c449a907b04c6f52 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:29:48 +0100 Subject: [PATCH 327/477] ci: revert global concurrency change again because it breaks PR workflows Need to figure out something better for this soon, but it's better not to have this at all right now --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9466cb083..209ed90de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,8 @@ name: Build Dalamud on: [push, pull_request, workflow_dispatch] -# Globally blocking because of git pushes in deploy step concurrency: - group: build_dalamud_${{ github.repository_owner }} + group: build_dalamud_${{ github.ref_name }} cancel-in-progress: false jobs: From 7f4352dc43df5d53c83b197603ee1383233c37a8 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Wed, 17 Dec 2025 16:38:34 -0800 Subject: [PATCH 328/477] Add address resolver --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 18 ++++ .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 91 ++++++++++--------- Dalamud/Plugin/Services/IAddonLifecycle.cs | 21 +++-- 3 files changed, 81 insertions(+), 49 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 716ce1bfb..78cea1a0f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.CompilerServices; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -132,6 +133,19 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } } + /// + /// Resolves a virtual table address to the original virtual table address. + /// + /// The modified address to resolve. + /// The original address. + internal AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress) + { + var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); + if (matchedTable == null) return null; + + return matchedTable.OriginalVirtualTable; + } + private void OnAddonInitialize(AtkUnitBase* addon) { try @@ -246,4 +260,8 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi }); } } + + /// + public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) + => (nint)this.addonLifecycleService.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 47ff92c3d..975ff027d 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -16,6 +16,16 @@ namespace Dalamud.Game.Addon.Lifecycle; /// internal unsafe class AddonVirtualTable : IDisposable { + /// + /// The original virtual table address for this addon. + /// + internal readonly AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable; + + /// + /// The modified virtual address for this addon. + /// + internal readonly AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable; + // This need to be at minimum the largest virtual table size of all addons // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; @@ -45,9 +55,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AtkUnitBase* atkUnitBase; - private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable; - private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable; - // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table, // the CLR needs to know they are in use, or it will invalidate them causing random crashing. private readonly AtkUnitBase.Delegates.Dtor destructorFunction; @@ -78,16 +85,16 @@ internal unsafe class AddonVirtualTable : IDisposable this.lifecycleService = lifecycleService; // Save original virtual table - this.originalVirtualTable = addon->VirtualTable; + this.OriginalVirtualTable = addon->VirtualTable; // Create copy of original table // Note this will copy any derived/overriden functions that this specific addon has. // Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game - this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8); - NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + this.ModifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8); + NativeMemory.Copy(addon->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount); // Overwrite the addons existing virtual table with our own - addon->VirtualTable = this.modifiedVirtualTable; + addon->VirtualTable = this.ModifiedVirtualTable; // Pin each of our listener functions this.destructorFunction = this.OnAddonDestructor; @@ -108,30 +115,30 @@ internal unsafe class AddonVirtualTable : IDisposable this.focusFunction = this.OnAddonFocus; // Overwrite specific virtual table entries - this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); - this.modifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction); - this.modifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction); - this.modifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction); - this.modifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); - this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction); - this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction); - this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction); - this.modifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction); - this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); - this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); - this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); - this.modifiedVirtualTable->OnMove = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction); - this.modifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); - this.modifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); - this.modifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); + this.ModifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); + this.ModifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction); + this.ModifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction); + this.ModifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction); + this.ModifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); + this.ModifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction); + this.ModifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction); + this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction); + this.ModifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction); + this.ModifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); + this.ModifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); + this.ModifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); + this.ModifiedVirtualTable->OnMove = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction); + this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); + this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); + this.ModifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); } /// public void Dispose() { // Ensure restoration is done atomically. - Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable); - IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.OriginalVirtualTable); + IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount); } private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) @@ -144,7 +151,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); + result = this.OriginalVirtualTable->Dtor(thisPtr, freeFlags); } catch (Exception e) { @@ -153,7 +160,7 @@ internal unsafe class AddonVirtualTable : IDisposable if ((freeFlags & 1) == 1) { - IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount); AddonLifecycle.AllocatedTables.Remove(this); } } @@ -182,7 +189,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnSetup(addon, valueCount, values); + this.OriginalVirtualTable->OnSetup(addon, valueCount, values); } catch (Exception e) { @@ -209,7 +216,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Finalizer(thisPtr); + this.OriginalVirtualTable->Finalizer(thisPtr); } catch (Exception e) { @@ -234,7 +241,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Draw(addon); + this.OriginalVirtualTable->Draw(addon); } catch (Exception e) { @@ -265,7 +272,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Update(addon, delta); + this.OriginalVirtualTable->Update(addon, delta); } catch (Exception e) { @@ -299,7 +306,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + result = this.OriginalVirtualTable->OnRefresh(addon, valueCount, values); } catch (Exception e) { @@ -333,7 +340,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + this.OriginalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); } catch (Exception e) { @@ -369,7 +376,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + this.OriginalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); } catch (Exception e) { @@ -398,7 +405,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - result = this.originalVirtualTable->Open(thisPtr, depthLayer); + result = this.OriginalVirtualTable->Open(thisPtr, depthLayer); } catch (Exception e) { @@ -432,7 +439,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - result = this.originalVirtualTable->Close(thisPtr, fireCallback); + result = this.OriginalVirtualTable->Close(thisPtr, fireCallback); } catch (Exception e) { @@ -466,7 +473,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + this.OriginalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); } catch (Exception e) { @@ -500,7 +507,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + this.OriginalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); } catch (Exception e) { @@ -527,7 +534,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnMove(thisPtr); + this.OriginalVirtualTable->OnMove(thisPtr); } catch (Exception e) { @@ -554,7 +561,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnMouseOver(thisPtr); + this.OriginalVirtualTable->OnMouseOver(thisPtr); } catch (Exception e) { @@ -581,7 +588,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnMouseOut(thisPtr); + this.OriginalVirtualTable->OnMouseOut(thisPtr); } catch (Exception e) { @@ -608,7 +615,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Focus(thisPtr); + this.OriginalVirtualTable->Focus(thisPtr); } catch (Exception e) { diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 1269b13dc..aeff29811 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -17,7 +17,7 @@ public interface IAddonLifecycle : IDalamudService /// The event type that triggered the message. /// Information about what addon triggered the message. public delegate void AddonEventDelegate(AddonEvent type, AddonArgs args); - + /// /// Register a listener that will trigger on the specified event and any of the specified addons. /// @@ -25,7 +25,7 @@ public interface IAddonLifecycle : IDalamudService /// Addon names that will trigger the handler to be invoked. /// The handler to invoke. void RegisterListener(AddonEvent eventType, IEnumerable addonNames, AddonEventDelegate handler); - + /// /// Register a listener that will trigger on the specified event only for the specified addon. /// @@ -33,14 +33,14 @@ public interface IAddonLifecycle : IDalamudService /// The addon name that will trigger the handler to be invoked. /// The handler to invoke. void RegisterListener(AddonEvent eventType, string addonName, AddonEventDelegate handler); - + /// /// Register a listener that will trigger on the specified event for any addon. /// /// Event type to trigger on. /// The handler to invoke. void RegisterListener(AddonEvent eventType, AddonEventDelegate handler); - + /// /// Unregister listener from specified event type and specified addon names. /// @@ -51,7 +51,7 @@ public interface IAddonLifecycle : IDalamudService /// Addon names to deregister. /// Optional specific handler to remove. void UnregisterListener(AddonEvent eventType, IEnumerable addonNames, [Optional] AddonEventDelegate handler); - + /// /// Unregister all listeners for the specified event type and addon name. /// @@ -62,7 +62,7 @@ public interface IAddonLifecycle : IDalamudService /// Addon name to deregister. /// Optional specific handler to remove. void UnregisterListener(AddonEvent eventType, string addonName, [Optional] AddonEventDelegate handler); - + /// /// Unregister an event type handler.
This will only remove a handler that is added via . ///
@@ -72,10 +72,17 @@ public interface IAddonLifecycle : IDalamudService /// Event type to deregister. /// Optional specific handler to remove. void UnregisterListener(AddonEvent eventType, [Optional] AddonEventDelegate handler); - + /// /// Unregister all events that use the specified handlers. /// /// Handlers to remove. void UnregisterListener(params AddonEventDelegate[] handlers); + + /// + /// Resolves an addons virtual table address back to the original unmodified table address. + /// + /// The address of a modified addons virtual table. + /// The address of the addons original virtual table. + nint GetOriginalVirtualTable(nint virtualTableAddress); } From 37fa40ab587002b4dd8180eb2872d5b5ef10fc37 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Wed, 17 Dec 2025 16:44:04 -0800 Subject: [PATCH 329/477] Make stylecop happy --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 975ff027d..736415738 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -16,16 +16,6 @@ namespace Dalamud.Game.Addon.Lifecycle; ///
internal unsafe class AddonVirtualTable : IDisposable { - /// - /// The original virtual table address for this addon. - /// - internal readonly AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable; - - /// - /// The modified virtual address for this addon. - /// - internal readonly AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable; - // This need to be at minimum the largest virtual table size of all addons // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; @@ -133,6 +123,16 @@ internal unsafe class AddonVirtualTable : IDisposable this.ModifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); } + /// + /// Gets the original virtual table address for this addon. + /// + internal AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable { get; private set; } + + /// + /// Gets the modified virtual address for this addon. + /// + internal AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable { get; private set; } + /// public void Dispose() { From 3a1e1e6425acf5be0a4721aae505f732f1cc0977 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 06:40:14 +0000 Subject: [PATCH 330/477] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index c74841abc..d8d0b53e2 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit c74841abce0830ead4437ed2f560bceb6235a538 +Subproject commit d8d0b53e27393f509ac5397511cb8d251d562277 From 17c0527f2d7b99ca988776071539528491942b5e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 06:40:20 +0000 Subject: [PATCH 331/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index c0a862043..90168316b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit c0a8620439e647ccf443710e81acce021b299bf5 +Subproject commit 90168316b4c5e3af2746a1bdea52fb10f9113862 From bd87a3d25156e25fab43a14caef9fde6a134d0ea Mon Sep 17 00:00:00 2001 From: Critical Impact Date: Thu, 18 Dec 2025 22:01:49 +1000 Subject: [PATCH 332/477] Add git hash/scm version properties to DalamudVersionInfo --- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- .../Plugin/VersionInfo/DalamudVersionInfo.cs | 11 ++++++++++- .../Plugin/VersionInfo/IDalamudVersionInfo.cs | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 90850a08b..e42bbe608 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -220,7 +220,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// public IDalamudVersionInfo GetDalamudVersion() { - return new DalamudVersionInfo(Versioning.GetAssemblyVersionParsed(), Versioning.GetActiveTrack()); + return new DalamudVersionInfo(Versioning.GetAssemblyVersionParsed(), Versioning.GetActiveTrack(), Versioning.GetGitHash(), Versioning.GetGitHashClientStructs(), Versioning.GetScmVersion()); } #region IPC diff --git a/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs index c87c012af..0a6fad9c2 100644 --- a/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs +++ b/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs @@ -1,11 +1,20 @@ namespace Dalamud.Plugin.VersionInfo; /// -internal class DalamudVersionInfo(Version version, string? track) : IDalamudVersionInfo +internal class DalamudVersionInfo(Version version, string? track, string? gitHash, string? gitHashClientStructs, string? scmVersion) : IDalamudVersionInfo { /// public Version Version { get; } = version; /// public string? BetaTrack { get; } = track; + + /// + public string? GitHash { get; } = gitHash; + + /// + public string? GitHashClientStructs { get; } = gitHashClientStructs; + + /// + public string? ScmVersion { get; } = scmVersion; } diff --git a/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs index e6b6a9601..6297ce196 100644 --- a/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs +++ b/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs @@ -16,4 +16,22 @@ public interface IDalamudVersionInfo /// Null if this build wasn't launched from XIVLauncher. ///
string? BetaTrack { get; } + + /// + /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds, + /// and will be suffixed with `-dirty` if in release with pending changes. + /// + string? GitHash { get; } + + /// + /// Gets the git hash value from the assembly or null if it cannot be found. + /// + string? GitHashClientStructs { get; } + + /// + /// Gets the SCM Version from the assembly, or null if it cannot be found. The value returned will generally be + /// the git describe output for this build, which will be a raw version if this is a stable build or an + /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string. + /// + string? ScmVersion { get; } } From 984bdbcf0ed616a040d0f344b502393f34ce4dbd Mon Sep 17 00:00:00 2001 From: Critical Impact Date: Thu, 18 Dec 2025 22:07:58 +1000 Subject: [PATCH 333/477] Use built dlls instead of csproj for docfx --- docfx.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docfx.json b/docfx.json index 30c85957c..cf7a80194 100644 --- a/docfx.json +++ b/docfx.json @@ -4,18 +4,17 @@ "src": [ { "files": [ - "Dalamud.Interface/Dalamud.Interface.csproj", - "Dalamud/Dalamud.csproj", - "lib/ImGuiScene/ImGuiScene/ImGuiScene.csproj", - "lib/ImGuiScene/deps/ImGui.NET/src/ImGui.NET-472/ImGui.NET-472.csproj", - "lib/ImGuiScene/deps/SDL2-CS/SDL2-CS.csproj" + "bin/Release/Dalamud.dll" ] } ], "dest": "api", "disableGitFeatures": false, "disableDefaultFilter": false, - "filter": "filterConfig.yml" + "filter": "filterConfig.yml", + "properties": { + "TargetFramework": "net10.0-windows" + } } ], "build": { From 0d533c18f8f80894c1f87f3cc001606c8dc3c656 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 13:21:49 +0000 Subject: [PATCH 334/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 90168316b..df206b5f6 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 90168316b4c5e3af2746a1bdea52fb10f9113862 +Subproject commit df206b5f61855e3ba73f93fd57bc07056698ac4a From 3c8cef06dd25bf189e136c3649d81f2f455c6551 Mon Sep 17 00:00:00 2001 From: Loskh <1020612624@qq.com> Date: Thu, 18 Dec 2025 21:54:28 +0800 Subject: [PATCH 335/477] fix: EventItem name for Japanese client. --- Dalamud/Utility/ItemUtil.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs index 5f718bcee..b632d14d7 100644 --- a/Dalamud/Utility/ItemUtil.cs +++ b/Dalamud/Utility/ItemUtil.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.Text; + using Lumina.Excel.Sheets; using Lumina.Text; using Lumina.Text.ReadOnly; @@ -125,10 +126,15 @@ public static class ItemUtil if (IsEventItem(itemId)) { + // Only English, German, and French have a Name field. + // For other languages, the Name is an empty string, and the Singular field should be used instead. + language ??= dataManager.Language; + var useSingular = language is not (ClientLanguage.English or ClientLanguage.German or ClientLanguage.French); + return dataManager .GetExcelSheet(language) .TryGetRow(itemId, out var eventItem) - ? eventItem.Name + ? (useSingular ? eventItem.Singular : eventItem.Name) : default; } From 0b1a697d4df81fc996bfc791c9ac8464fec01512 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 18 Dec 2025 15:38:57 +0100 Subject: [PATCH 336/477] - Comment out erroring unknown prints --- Dalamud/Interface/Internal/UiDebug.cs | 14 ++++++------ .../UiDebug2/Browsing/NodeTree.Component.cs | 22 +++++++------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 1211b505d..82554995b 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -420,13 +420,13 @@ internal unsafe class UiDebug ImGui.SameLine(); Service.Get().Draw(textInputComponent->AtkComponentInputBase.RawString); - ImGui.Text("Text1: "u8); - ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText01); - - ImGui.Text("Text2: "u8); - ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText02); + // ImGui.Text("Text1: "u8); + // ImGui.SameLine(); + // Service.Get().Draw(textInputComponent->UnkText01); + // + // ImGui.Text("Text2: "u8); + // ImGui.SameLine(); + // Service.Get().Draw(textInputComponent->UnkText02); ImGui.Text("AvailableLines: "u8); ImGui.SameLine(); diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs index a35195498..922d226b6 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs @@ -89,20 +89,14 @@ internal unsafe class ComponentNodeTree : ResNodeTree { case TextInput: var textInputComponent = (AtkComponentTextInput*)this.Component; - ImGui.Text( - $"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.EvaluatedString.StringPtr))}"); - ImGui.Text( - $"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.RawString.StringPtr))}"); - ImGui.Text( - $"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}"); - ImGui.Text( - $"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}"); - ImGui.Text( - $"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}"); - ImGui.Text( - $"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}"); - ImGui.Text( - $"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}"); + ImGui.Text($"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.EvaluatedString.StringPtr))}"); + ImGui.Text($"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.RawString.StringPtr))}"); + // TODO: Reenable when unknowns have been unprivated / named + // ImGui.Text($"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}"); + // ImGui.Text($"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}"); + ImGui.Text($"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}"); + ImGui.Text($"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}"); + ImGui.Text($"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}"); break; case List: case TreeList: From 3eb65c85c06c357c3af89725f75213caa4825cbf Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:08:27 +0100 Subject: [PATCH 337/477] Update ClientStructs (#2524) 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 df206b5f6..a88271426 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit df206b5f61855e3ba73f93fd57bc07056698ac4a +Subproject commit a8827142678d35e62ab0c1bafe94d607271af010 From a56d2cf40be3a09c3d9105e722c2c84492e90887 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Dec 2025 20:28:03 +0100 Subject: [PATCH 338/477] Add verifier for hook signatures This one is real bad, so we should make sure everyone using a canonical signature --- Dalamud/Dalamud.cs | 6 + Dalamud/Hooking/Hook.cs | 3 + .../Verification/HookVerificationException.cs | 41 +++++++ .../Internal/Verification/HookVerifier.cs | 107 ++++++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 Dalamud/Hooking/Internal/Verification/HookVerificationException.cs create mode 100644 Dalamud/Hooking/Internal/Verification/HookVerifier.cs diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index a411883d5..2d32b8e8a 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; +using Dalamud.Hooking.Internal.Verification; using Dalamud.Plugin.Internal; using Dalamud.Storage; using Dalamud.Utility; @@ -73,6 +74,11 @@ internal sealed unsafe class Dalamud : IServiceType scanner, Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride)); + using (Timings.Start("HookVerifier Init")) + { + HookVerifier.Initialize(scanner); + } + // Set up FFXIVClientStructs this.SetupClientStructsResolver(cacheDir); diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 1cd3ef91d..b8fd78b4f 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; using Dalamud.Hooking.Internal; +using Dalamud.Hooking.Internal.Verification; namespace Dalamud.Hooking; @@ -230,6 +231,8 @@ public abstract class Hook : IDalamudHook where T : Delegate if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; + HookVerifier.Verify(procAddress); + procAddress = HookManager.FollowJmp(procAddress); if (useMinHook) return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); diff --git a/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs b/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs new file mode 100644 index 000000000..c43b5d540 --- /dev/null +++ b/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs @@ -0,0 +1,41 @@ +using System.Linq; + +namespace Dalamud.Hooking.Internal.Verification; + +/// +/// Exception thrown when a provided delegate for a hook does not match a known delegate. +/// +public class HookVerificationException : Exception +{ + private HookVerificationException(string message) + : base(message) + { + } + + /// + /// Create a new exception. + /// + /// The address of the function that is being hooked. + /// The delegate passed by the user. + /// The delegate we think is correct. + /// Additional context to show to the user. + /// The created exception. + internal static HookVerificationException Create(IntPtr address, Type passed, Type enforced, string message) + { + return new HookVerificationException( + $"Hook verification failed for address 0x{address.ToInt64():X}\n\n" + + $"Why: {message}\n" + + $"Passed Delegate: {GetSignature(passed)}\n" + + $"Correct Delegate: {GetSignature(enforced)}\n\n" + + "The hook delegate must exactly match the provided signature to prevent memory corruption and wrong data passed to originals."); + } + + private static string GetSignature(Type delegateType) + { + var method = delegateType.GetMethod("Invoke"); + if (method == null) return delegateType.Name; + + var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name)); + return $"{method.ReturnType.Name} ({parameters})"; + } +} diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs new file mode 100644 index 000000000..ad68ae38e --- /dev/null +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -0,0 +1,107 @@ +using System.Linq; + +using Dalamud.Game; +using Dalamud.Logging.Internal; + +namespace Dalamud.Hooking.Internal.Verification; + +/// +/// Global utility that can verify whether hook delegates are correctly declared. +/// Initialized out-of-band, since Hook is instantiated all over the place without a service, so this cannot be +/// a service either. +/// +internal static class HookVerifier +{ + private static readonly ModuleLog Log = new("HookVerifier"); + + private static readonly VerificationEntry[] ToVerify = + [ + new( + "ActorControlSelf", + "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", + typeof(ActorControlSelfDelegate), + "Signature changed in Patch 7.4") // 7.4 (new parameters) + ]; + + private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); + + /// + /// Initializes a new instance of the class. + /// + /// Process to scan in. + public static void Initialize(TargetSigScanner scanner) + { + foreach (var entry in ToVerify) + { + if (!scanner.TryScanText(entry.Signature, out var address)) + { + Log.Error("Could not resolve signature for hook {Name} ({Sig})", entry.Name, entry.Signature); + continue; + } + + entry.Address = address; + } + } + + /// + /// Verify the hook with the provided address and exception. + /// + /// The address of the function we are hooking. + /// The delegate type passed by the creator of the hook. + /// Exception thrown when we think the hook is not correctly declared. + public static void Verify(IntPtr address) where T : Delegate + { + var entry = ToVerify.FirstOrDefault(x => x.Address == address); + + // Nothing to verify for this hook? + if (entry == null) + { + return; + } + + var passedType = typeof(T); + + // Directly compare delegates + if (passedType == entry.TargetDelegateType) + { + return; + } + + var passedInvoke = passedType.GetMethod("Invoke")!; + var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; + + // Compare Return Type + var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType; + + // Compare Parameter Count + var passedParams = passedInvoke.GetParameters(); + var enforcedParams = enforcedInvoke.GetParameters(); + + if (passedParams.Length != enforcedParams.Length) + { + mismatch = true; + } + else + { + // Compare Parameter Types + for (var i = 0; i < passedParams.Length; i++) + { + if (passedParams[i].ParameterType != enforcedParams[i].ParameterType) + { + mismatch = true; + break; + } + } + } + + if (mismatch) + { + throw HookVerificationException.Create(address, passedType, entry.TargetDelegateType, entry.Message); + } + } + + private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message) + { + public nint Address { get; set; } + } +} From 19fca721e9f6e68a292b04ba9968e4f1454c934c Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Dec 2025 20:55:04 +0100 Subject: [PATCH 339/477] Make obsoletions for ClientState error --- Dalamud/Plugin/Services/IClientState.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 2555b3b30..28b8494b2 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -109,13 +109,13 @@ public interface IClientState : IDalamudService /// /// Gets the local player character, if one is present. /// - [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.")] + [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.", true)] public IPlayerCharacter? LocalPlayer { get; } /// /// Gets the content ID of the local character. /// - [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}")] + [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}", true)] public ulong LocalContentId { get; } /// From c005bae265a49ed33217d97151d863fb92a03324 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Dec 2025 21:00:07 +0100 Subject: [PATCH 340/477] Revert obsolete as error again, fix warnings, Api14ToDo => Api15ToDo --- Dalamud/Configuration/PluginConfigurations.cs | 2 +- .../AddonArgTypes/AddonRefreshArgs.cs | 2 ++ .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 2 ++ Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +- Dalamud/Interface/Animation/Easing.cs | 2 +- .../Plugin/Ipc/Internal/CallGateChannel.cs | 11 +++++++++ Dalamud/Plugin/Services/IClientState.cs | 7 ++++-- Dalamud/Utility/Api14ToDoAttribute.cs | 24 ------------------- 8 files changed, 23 insertions(+), 29 deletions(-) delete mode 100644 Dalamud/Utility/Api14ToDoAttribute.cs diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index c01ab2af0..7ce4697cb 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -11,7 +11,7 @@ namespace Dalamud.Configuration; /// /// Configuration to store settings for a dalamud plugin. /// -[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] +[Api15ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index 4fc81632a..d81d262bf 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -55,7 +55,9 @@ public class AddonRefreshArgs : AddonArgs AtkValuePtr ptr; unsafe { +#pragma warning disable CS0618 // Type or member is obsolete ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); +#pragma warning restore CS0618 // Type or member is obsolete } yield return ptr; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index e0b2defbf..1cc0eacf3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -55,7 +55,9 @@ public class AddonSetupArgs : AddonArgs AtkValuePtr ptr; unsafe { +#pragma warning disable CS0618 // Type or member is obsolete ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); +#pragma warning restore CS0618 // Type or member is obsolete } yield return ptr; diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index af85f9228..138484580 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry } /// - [Api14ToDo("Maybe make this config scoped to internal name?")] + [Api15ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index cc1f48ce7..a9dfad1f0 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -48,7 +48,7 @@ public abstract class Easing /// Gets the current value of the animation, following unclamped logic. /// [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] - [Api14ToDo("Map this field to ValueClamped, probably.")] + [Api15ToDo("Map this field to ValueClamped, probably.")] public double Value => this.ValueUnclamped; /// diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index e177abab7..8bd631b0e 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -149,16 +149,27 @@ internal class CallGateChannel return (TRet)result; } + /// + /// Set the context for the invocations through this channel. + /// + /// The context to set. internal void SetInvocationContext(IpcContext ipcContext) { this.ipcExecutionContext.Value = ipcContext; } + /// + /// Get the context for invocations through this channel. + /// + /// The context, if one was set. internal IpcContext? GetInvocationContext() { return this.ipcExecutionContext.IsValueCreated ? this.ipcExecutionContext.Value : null; } + /// + /// Clear the context for this channel. + /// internal void ClearInvocationContext() { this.ipcExecutionContext.Value = null; diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 28b8494b2..9e7453c25 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -2,6 +2,7 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Utility; namespace Dalamud.Plugin.Services; @@ -109,13 +110,15 @@ public interface IClientState : IDalamudService /// /// Gets the local player character, if one is present. /// - [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.", true)] + [Api15ToDo("Remove")] + [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.")] public IPlayerCharacter? LocalPlayer { get; } /// /// Gets the content ID of the local character. /// - [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}", true)] + [Api15ToDo("Remove")] + [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}")] public ulong LocalContentId { get; } /// diff --git a/Dalamud/Utility/Api14ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs deleted file mode 100644 index 945b6e4db..000000000 --- a/Dalamud/Utility/Api14ToDoAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dalamud.Utility; - -/// -/// Utility class for marking something to be changed for API 13, for ease of lookup. -/// -[AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api14ToDoAttribute : Attribute -{ - /// - /// Marks that this should be made internal. - /// - public const string MakeInternal = "Make internal."; - - /// - /// Initializes a new instance of the class. - /// - /// The explanation. - /// The explanation 2. - public Api14ToDoAttribute(string what, string what2 = "") - { - _ = what; - _ = what2; - } -} From db5f27518fb6ec50cdfa5d6dcd39e10cbb4f1fd7 Mon Sep 17 00:00:00 2001 From: CMDRNuffin <4348470+CMDRNuffin@users.noreply.github.com> Date: Fri, 19 Dec 2025 01:24:43 +0100 Subject: [PATCH 341/477] Prevent ImGui text box methods from cloning unchanged input every frame The overloads taking a string by ref for the input text of the various ways to display a text box would all take the input string, copy it into a buffer for imgui and then unconditionally produce a new string once the imgui call returned. Now we only create a new string when the return value of the native function actually indicates that the text changed. This makes the GC happy, and also users like me who like to make the GC happy. Other side effects: The assumption that the reference doesn't change if the method returns false, which is very reasonable IMO, is now correct. --- .../Custom/ImGui.Manual.cs | 153 ++++++++++++++---- 1 file changed, 119 insertions(+), 34 deletions(-) diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs index 89b3cc3d6..ce1bf961d 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs @@ -127,8 +127,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -140,8 +145,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -153,8 +163,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -166,8 +181,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -287,8 +307,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -300,8 +325,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -314,8 +344,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -328,8 +363,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -388,8 +428,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -401,8 +446,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -414,8 +464,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -427,8 +482,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -477,8 +537,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -490,8 +555,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -503,8 +573,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -516,8 +591,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -541,8 +621,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = TempInputText(bb, id, label, t.Buffer[..(maxLength + 1)], flags); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } From 86e12f411d672a511ee3e51d6660ce3b6f4f489c Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 19 Dec 2025 03:26:53 +0100 Subject: [PATCH 342/477] Update UIColor widget --- .../Windows/Data/Widgets/UIColorWidget.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index e52a291ef..3550f053c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -44,7 +44,7 @@ internal class UiColorWidget : IDataWindowWidget "BB.
" + "· Click on a color to copy the color code.
" + "· Hover on a color to preview the text with edge, when the next color has been used together."); - if (!ImGui.BeginTable("UIColor"u8, 5)) + if (!ImGui.BeginTable("UIColor"u8, 7)) return; ImGui.TableSetupScrollFreeze(0, 1); @@ -62,6 +62,8 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableSetupColumn("Light"u8, ImGuiTableColumnFlags.WidthFixed, colorw); ImGui.TableSetupColumn("Classic FF"u8, ImGuiTableColumnFlags.WidthFixed, colorw); ImGui.TableSetupColumn("Clear Blue"u8, ImGuiTableColumnFlags.WidthFixed, colorw); + ImGui.TableSetupColumn("Clear White"u8, ImGuiTableColumnFlags.WidthFixed, colorw); + ImGui.TableSetupColumn("Clear Green"u8, ImGuiTableColumnFlags.WidthFixed, colorw); ImGui.TableHeadersRow(); var clipper = ImGui.ImGuiListClipper(); @@ -120,6 +122,22 @@ internal class UiColorWidget : IDataWindowWidget adjacentRow.HasValue) DrawEdgePreview(id, row.ClearBlue, adjacentRow.Value.ClearBlue); ImGui.PopID(); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.PushID($"row{id}_white"); + if (this.DrawColorColumn(row.Unknown0) && + adjacentRow.HasValue) + DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0); + ImGui.PopID(); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.PushID($"row{id}_green"); + if (this.DrawColorColumn(row.Unknown1) && + adjacentRow.HasValue) + DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1); + ImGui.PopID(); } } From 7eea7d6182c6becc92175b158d6782038d9045af Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 19 Dec 2025 03:35:52 +0100 Subject: [PATCH 343/477] Update ClientStructs (#2525) 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 a88271426..f60c282d6 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit a8827142678d35e62ab0c1bafe94d607271af010 +Subproject commit f60c282d63b4157a8f8fb7cbb7e0b35361cdaa12 From 7af0523e886f314461229067a541c993f1498e43 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 06:38:57 +0000 Subject: [PATCH 344/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index f60c282d6..7227f6b12 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit f60c282d63b4157a8f8fb7cbb7e0b35361cdaa12 +Subproject commit 7227f6b1222d1149e0b2e26d2dc31acf341df1cc From f3f4ced0495781bedd04d0345576f695699c4bb6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 06:39:07 +0000 Subject: [PATCH 345/477] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index d8d0b53e2..7d3f90e61 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit d8d0b53e27393f509ac5397511cb8d251d562277 +Subproject commit 7d3f90e61732df6aef63196d1abaab1074f6f3c9 From 89c46944b6832e6e562009c6aa756a5b871fef2f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 10:20:39 +0000 Subject: [PATCH 346/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 7227f6b12..faf803a76 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7227f6b1222d1149e0b2e26d2dc31acf341df1cc +Subproject commit faf803a76813511768d45c137a543aaacf5420b8 From 4ddaaf3809e82adab97d6761931ece609b7f7c2f Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:41:33 +0100 Subject: [PATCH 347/477] Add new themes and update themed path logic --- .../Interface/Internal/Windows/Data/Widgets/UldWidget.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index bc12f4d28..019154b53 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -26,8 +26,8 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class UldWidget : IDataWindowWidget { // ULD styles can be hardcoded for now as they don't add new ones regularly. Can later try and find where to load these from in the game EXE. - private static readonly string[] ThemeDisplayNames = ["Dark", "Light", "Classic FF", "Clear Blue"]; - private static readonly string[] ThemeBasePaths = ["ui/uld/", "ui/uld/img01/", "ui/uld/img02/", "ui/uld/img03/"]; + private static readonly string[] ThemeDisplayNames = ["Dark", "Light", "Classic FF", "Clear Blue", "Clear White", "Clear Green"]; + private const string UldBaseBath = "ui/uld/"; // 48 8D 15 ?? ?? ?? ?? is the part of the signatures that contain the string location offset // 48 = 64 bit register prefix @@ -263,7 +263,7 @@ internal class UldWidget : IDataWindowWidget } private string ToThemedPath(string path) => - ThemeBasePaths[this.selectedTheme] + path[ThemeBasePaths[0].Length..]; + this.UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : "") + path[this.UldBaseBath.Length..]; private void DrawTextureEntry(UldRoot.TextureEntry textureEntry, TextureManager textureManager) { From c71d8889d791a3ba48dcb290c37ce63f148e70de Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:51:09 +0100 Subject: [PATCH 348/477] Access const as non instance --- Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index 019154b53..4d858922a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -263,7 +263,7 @@ internal class UldWidget : IDataWindowWidget } private string ToThemedPath(string path) => - this.UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : "") + path[this.UldBaseBath.Length..]; + UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : "") + path[UldBaseBath.Length..]; private void DrawTextureEntry(UldRoot.TextureEntry textureEntry, TextureManager textureManager) { From a3d930b8e2843f560ab2d850d793e0b90d228c8b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 19 Dec 2025 17:34:42 +0100 Subject: [PATCH 349/477] Use RentedSeStringBuilder more --- Dalamud/Game/Gui/ChatGui.cs | 41 ++- Dalamud/Game/Internal/DalamudCompletion.cs | 8 +- .../Game/Text/Evaluator/SeStringEvaluator.cs | 289 ++++++++---------- Dalamud/Game/Text/Noun/NounProcessor.cs | 105 +++---- .../Payloads/AutoTranslatePayload.cs | 16 +- .../Payloads/DalamudLinkPayload.cs | 27 +- .../Payloads/PlayerPayload.cs | 17 +- .../Game/Text/SeStringHandling/SeString.cs | 6 +- .../Data/Widgets/SeStringCreatorWidget.cs | 33 +- .../Steps/GamepadStateSelfTestStep.cs | 26 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 25 +- Dalamud/Utility/ItemUtil.cs | 13 +- Dalamud/Utility/SeStringExtensions.cs | 36 +-- 13 files changed, 275 insertions(+), 367 deletions(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index d7303c4ce..30e2b676c 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -26,7 +26,6 @@ using Lumina.Text; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; using SeString = Dalamud.Game.Text.SeStringHandling.SeString; using SeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder; @@ -207,21 +206,21 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui if (this.chatQueue.Count == 0) return; - var sb = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); Span namebuf = stackalloc byte[256]; using var sender = new Utf8String(); using var message = new Utf8String(); while (this.chatQueue.TryDequeue(out var chat)) { - sb.Clear(); + rssb.Builder.Clear(); foreach (var c in UtfEnumerator.From(chat.MessageBytes, UtfEnumeratorFlags.Utf8SeString)) { if (c.IsSeStringPayload) - sb.Append((ReadOnlySeStringSpan)chat.MessageBytes.AsSpan(c.ByteOffset, c.ByteLength)); + rssb.Builder.Append((ReadOnlySeStringSpan)chat.MessageBytes.AsSpan(c.ByteOffset, c.ByteLength)); else if (c.Value.IntValue == 0x202F) - sb.BeginMacro(MacroCode.NonBreakingSpace).EndMacro(); + rssb.Builder.BeginMacro(MacroCode.NonBreakingSpace).EndMacro(); else - sb.Append(c); + rssb.Builder.Append(c); } if (chat.NameBytes.Length + 1 < namebuf.Length) @@ -235,7 +234,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui sender.SetString(chat.NameBytes.NullTerminate()); } - message.SetString(sb.GetViewAsSpan()); + message.SetString(rssb.Builder.GetViewAsSpan()); var targetChannel = chat.Type ?? this.configuration.GeneralChatType; @@ -247,8 +246,6 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui chat.Timestamp, (byte)(chat.Silent ? 1 : 0)); } - - LSeStringBuilder.SharedPool.Return(sb); } /// @@ -326,29 +323,28 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui private void PrintTagged(ReadOnlySpan message, XivChatType channel, string? tag, ushort? color) { - var sb = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); if (!tag.IsNullOrEmpty()) { if (color is not null) { - sb.PushColorType(color.Value); - sb.Append($"[{tag}] "); - sb.PopColorType(); + rssb.Builder + .PushColorType(color.Value) + .Append($"[{tag}] ") + .PopColorType(); } else { - sb.Append($"[{tag}] "); + rssb.Builder.Append($"[{tag}] "); } } this.Print(new XivChatEntry { - MessageBytes = sb.Append((ReadOnlySeStringSpan)message).ToArray(), + MessageBytes = rssb.Builder.Append((ReadOnlySeStringSpan)message).ToArray(), Type = channel, }); - - LSeStringBuilder.SharedPool.Return(sb); } private void InventoryItemCopyDetour(InventoryItem* thisPtr, InventoryItem* otherPtr) @@ -457,7 +453,8 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); - var sb = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); + try { var seStringSpan = new ReadOnlySeStringSpan(linkData->Payload); @@ -465,7 +462,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui // read until link terminator foreach (var payload in seStringSpan) { - sb.Append(payload); + rssb.Builder.Append(payload); if (payload.Type == ReadOnlySePayloadType.Macro && payload.MacroCode == MacroCode.Link && @@ -477,7 +474,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui } } - var seStr = SeString.Parse(sb.ToArray()); + var seStr = SeString.Parse(rssb.Builder.ToArray()); if (seStr.Payloads.Count == 0 || seStr.Payloads[0] is not DalamudLinkPayload link) return; @@ -495,10 +492,6 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui { Log.Error(ex, "Exception in HandleLinkClickDetour"); } - finally - { - LSeStringBuilder.SharedPool.Return(sb); - } } } diff --git a/Dalamud/Game/Internal/DalamudCompletion.cs b/Dalamud/Game/Internal/DalamudCompletion.cs index e3564c823..50816a603 100644 --- a/Dalamud/Game/Internal/DalamudCompletion.cs +++ b/Dalamud/Game/Internal/DalamudCompletion.cs @@ -11,8 +11,6 @@ using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.Completion; using FFXIVClientStructs.FFXIV.Component.GUI; -using Lumina.Text; - namespace Dalamud.Game.Internal; /// @@ -253,16 +251,14 @@ internal sealed unsafe class DalamudCompletion : IInternalDisposableService { public EntryStrings(string command) { - var rssb = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - this.Display = Utf8String.FromSequence(rssb + this.Display = Utf8String.FromSequence(rssb.Builder .PushColorType(539) .Append(command) .PopColorType() .GetViewAsSpan()); - SeStringBuilder.SharedPool.Return(rssb); - this.Match = Utf8String.FromString(command); } diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 58bcdbd0b..f05c15263 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -102,16 +102,15 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator // TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append, // and then remove try...finally block (discard builder from the pool on exception) var previousCulture = CultureInfo.CurrentCulture; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); try { CultureInfo.CurrentCulture = Localization.GetCultureInfoFromLangCode(lang.ToCode()); - return this.EvaluateAndAppendTo(builder, str, localParameters, lang).ToReadOnlySeString(); + return this.EvaluateAndAppendTo(rssb.Builder, str, localParameters, lang).ToReadOnlySeString(); } finally { CultureInfo.CurrentCulture = previousCulture; - SeStringBuilder.SharedPool.Return(builder); } } @@ -930,7 +929,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator itemId += 1000000; } - var sb = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); + var sb = rssb.Builder; sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); @@ -956,7 +956,6 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator sb.PopLink(); text = sb.ToReadOnlySeString(); - SeStringBuilder.SharedPool.Return(sb); } private void CreateSheetLink(in SeStringContext context, string resolvedSheetName, ReadOnlySeString text, uint eRowIdValue, uint eColParamValue) @@ -1028,40 +1027,33 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + var pIdx = 0; + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + pIdx++; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - var str = builder.ToReadOnlySeString(); - var pIdx = 0; - - foreach (var p in str) + if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) { - pIdx++; - - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; - - if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToUpper(context.CultureInfo)); - continue; - } - - context.Builder.Append(p); + context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToUpper(context.CultureInfo)); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveHead(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1069,40 +1061,33 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + var pIdx = 0; + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + pIdx++; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - var str = builder.ToReadOnlySeString(); - var pIdx = 0; - - foreach (var p in str) + if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) { - pIdx++; - - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; - - if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToUpper(context.CultureInfo)); - continue; - } - - context.Builder.Append(p); + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToUpper(context.CultureInfo)); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveSplit(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1113,32 +1098,25 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!eSeparator.TryGetString(out var eSeparatorVal) || !eIndex.TryGetUInt(out var eIndexVal) || eIndexVal <= 0) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try - { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); - - if (!this.ResolveStringExpression(headContext, eText)) - return false; - - var separator = eSeparatorVal.ExtractText(); - if (separator.Length < 1) - return false; - - var splitted = builder.ToReadOnlySeString().ExtractText().Split(separator[0]); - if (eIndexVal <= splitted.Length) - { - context.Builder.Append(splitted[eIndexVal - 1]); - return true; - } + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + if (!this.ResolveStringExpression(headContext, eText)) return false; - } - finally + + var separator = eSeparatorVal.ExtractText(); + if (separator.Length < 1) + return false; + + var splitted = rssb.Builder.ToReadOnlySeString().ExtractText().Split(separator[0]); + if (eIndexVal <= splitted.Length) { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(splitted[eIndexVal - 1]); + return true; } + + return false; } private bool TryResolveHeadAll(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1146,37 +1124,30 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; - - var str = builder.ToReadOnlySeString(); - - foreach (var p in str) + if (p.Type == ReadOnlySePayloadType.Text) { - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; - - if (p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language)); - continue; - } - - context.Builder.Append(p); + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language)); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveFixed(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1306,14 +1277,13 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!this.dataManager.GetExcelSheet().TryGetRow(mapId, out var mapRow)) return false; - var sb = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - sb.Append(placeNameRow.Name); + rssb.Builder.Append(placeNameRow.Name); if (instance is > 0 and <= 9) - sb.Append((char)((char)0xE0B0 + (char)instance)); + rssb.Builder.Append((char)((char)0xE0B0 + (char)instance)); - var placeNameWithInstance = sb.ToReadOnlySeString(); - SeStringBuilder.SharedPool.Return(sb); + var placeNameWithInstance = rssb.Builder.ToReadOnlySeString(); var mapPosX = ConvertRawToMapPosX(mapRow, rawX / 1000f); var mapPosY = ConvertRawToMapPosY(mapRow, rawY / 1000f); @@ -1462,23 +1432,22 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator statusDescription = statusRow.Description.AsSpan(); } - var sb = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); switch (statusRow.StatusCategory) { case 1: - sb.Append(this.EvaluateFromAddon(376, default, context.Language)); + rssb.Builder.Append(this.EvaluateFromAddon(376, default, context.Language)); break; case 2: - sb.Append(this.EvaluateFromAddon(377, default, context.Language)); + rssb.Builder.Append(this.EvaluateFromAddon(377, default, context.Language)); break; } - sb.Append(statusName); + rssb.Builder.Append(statusName); - var linkText = sb.ToReadOnlySeString(); - SeStringBuilder.SharedPool.Return(sb); + var linkText = rssb.Builder.ToReadOnlySeString(); context.Builder .BeginMacro(MacroCode.Link) @@ -1733,38 +1702,31 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; - - var str = builder.ToReadOnlySeString(); - - foreach (var p in str) + if (p.Type == ReadOnlySePayloadType.Text) { - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; + context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToLower(context.CultureInfo)); - if (p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToLower(context.CultureInfo)); - - continue; - } - - context.Builder.Append(p); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveNoun(ClientLanguage language, in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1834,40 +1796,33 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + var pIdx = 0; + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + pIdx++; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - var str = builder.ToReadOnlySeString(); - var pIdx = 0; - - foreach (var p in str) + if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) { - pIdx++; - - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; - - if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToLower(context.CultureInfo)); - continue; - } - - context.Builder.Append(p); + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToLower(context.CultureInfo)); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveColorType(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -2132,19 +2087,19 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (operand1.TryGetString(out var strval1) && operand2.TryGetString(out var strval2)) { + using var rssb1 = new RentedSeStringBuilder(); + using var rssb2 = new RentedSeStringBuilder(); var resolvedStr1 = this.EvaluateAndAppendTo( - SeStringBuilder.SharedPool.Get(), + rssb1.Builder, strval1, context.LocalParameters, context.Language); var resolvedStr2 = this.EvaluateAndAppendTo( - SeStringBuilder.SharedPool.Get(), + rssb2.Builder, strval2, context.LocalParameters, context.Language); var equals = resolvedStr1.GetViewAsSpan().SequenceEqual(resolvedStr2.GetViewAsSpan()); - SeStringBuilder.SharedPool.Return(resolvedStr1); - SeStringBuilder.SharedPool.Return(resolvedStr2); if ((ExpressionType)exprType == ExpressionType.Equal) value = equals ? 1u : 0u; diff --git a/Dalamud/Game/Text/Noun/NounProcessor.cs b/Dalamud/Game/Text/Noun/NounProcessor.cs index 18f8cd4a9..993d341df 100644 --- a/Dalamud/Game/Text/Noun/NounProcessor.cs +++ b/Dalamud/Game/Text/Noun/NounProcessor.cs @@ -9,7 +9,6 @@ using Dalamud.Utility; using Lumina.Excel; using Lumina.Text.ReadOnly; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; using LSheets = Lumina.Excel.Sheets; namespace Dalamud.Game.Text.Noun; @@ -147,30 +146,28 @@ internal class NounProcessor : IServiceType var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); - var builder = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); // Ko-So-A-Do var ksad = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(nounParams.Quantity > 1 ? 1 : 0); if (!ksad.IsEmpty) { - builder.Append(ksad); + rssb.Builder.Append(ksad); if (nounParams.Quantity > 1) { - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); } } if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); var text = row.ReadStringColumn(nounParams.ColumnOffset); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); - var ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } /// @@ -200,7 +197,7 @@ internal class NounProcessor : IServiceType var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); - var builder = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); var isProperNounColumn = nounParams.ColumnOffset + ArticleColumnIdx; var isProperNoun = isProperNounColumn >= 0 ? row.ReadInt8Column(isProperNounColumn) : ~isProperNounColumn; @@ -216,21 +213,19 @@ internal class NounProcessor : IServiceType var article = attributiveSheet.GetRow((uint)nounParams.ArticleType) .ReadStringColumn(articleColumn + grammaticalNumberColumnOffset); if (!article.IsEmpty) - builder.Append(article); + rssb.Builder.Append(article); if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); } var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx)); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - var ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } /// @@ -262,17 +257,13 @@ internal class NounProcessor : IServiceType var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); - var builder = LSeStringBuilder.SharedPool.Get(); - ReadOnlySeString ross; + using var rssb = new RentedSeStringBuilder(); if (nounParams.IsActionSheet) { - builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase)); - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - - ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + rssb.Builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase)); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + return rssb.Builder.ToReadOnlySeString(); } var genderIndexColumn = nounParams.ColumnOffset + PronounColumnIdx; @@ -302,35 +293,32 @@ internal class NounProcessor : IServiceType var grammaticalGender = attributiveSheet.GetRow((uint)nounParams.ArticleType) .ReadStringColumn(caseColumnOffset + genderIndex); // Genus if (!grammaticalGender.IsEmpty) - builder.Append(grammaticalGender); + rssb.Builder.Append(grammaticalGender); } if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); - builder.Append(text); + rssb.Builder.Append(text); var plural = attributiveSheet.GetRow((uint)(caseRowOffset + 26)) .ReadStringColumn(caseColumnOffset + genderIndex); - if (builder.ContainsText("[p]"u8)) - builder.ReplaceText("[p]"u8, plural); + if (rssb.Builder.ContainsText("[p]"u8)) + rssb.Builder.ReplaceText("[p]"u8, plural); else - builder.Append(plural); + rssb.Builder.Append(plural); if (hasT) { var article = attributiveSheet.GetRow(39).ReadStringColumn(caseColumnOffset + genderIndex); // Definiter Artikel - builder.ReplaceText("[t]"u8, article); + rssb.Builder.ReplaceText("[t]"u8, article); } } - var pa = attributiveSheet.GetRow(24).ReadStringColumn(caseColumnOffset + genderIndex); - builder.ReplaceText("[pa]"u8, pa); + rssb.Builder.ReplaceText("[pa]"u8, attributiveSheet.GetRow(24).ReadStringColumn(caseColumnOffset + genderIndex)); - RawRow declensionRow; - - declensionRow = (GermanArticleType)nounParams.ArticleType switch + var declensionRow = (GermanArticleType)nounParams.ArticleType switch { // Schwache Flexion eines Adjektivs?! GermanArticleType.Possessive or GermanArticleType.Demonstrative => attributiveSheet.GetRow(25), @@ -347,14 +335,10 @@ internal class NounProcessor : IServiceType _ => attributiveSheet.GetRow(26), }; - var declension = declensionRow.ReadStringColumn(caseColumnOffset + genderIndex); - builder.ReplaceText("[a]"u8, declension); + rssb.Builder.ReplaceText("[a]"u8, declensionRow.ReadStringColumn(caseColumnOffset + genderIndex)); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - - ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } /// @@ -385,8 +369,7 @@ internal class NounProcessor : IServiceType var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); - var builder = LSeStringBuilder.SharedPool.Get(); - ReadOnlySeString ross; + using var rssb = new RentedSeStringBuilder(); var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx; var startsWithVowel = startsWithVowelColumn >= 0 @@ -405,21 +388,19 @@ internal class NounProcessor : IServiceType { var v21 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20); if (!v21.IsEmpty) - builder.Append(v21); + rssb.Builder.Append(v21); if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity <= 1 ? SingularColumnIdx : PluralColumnIdx)); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); if (nounParams.Quantity <= 1) - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } var v17 = row.ReadInt8Column(nounParams.ColumnOffset + Unknown5ColumnIdx); @@ -428,34 +409,32 @@ internal class NounProcessor : IServiceType var v29 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + 2); if (!v29.IsEmpty) { - builder.Append(v29); + rssb.Builder.Append(v29); if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); var text = row.ReadStringColumn(nounParams.ColumnOffset + PluralColumnIdx); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); } } else { var v27 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + (v17 != 0 ? 1 : 3)); if (!v27.IsEmpty) - builder.Append(v27); + rssb.Builder.Append(v27); if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); var text = row.ReadStringColumn(nounParams.ColumnOffset + SingularColumnIdx); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); } - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } } diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs index 470e942c3..8178a6d33 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs @@ -1,6 +1,7 @@ using System.IO; using Dalamud.Game.Text.Evaluator; +using Dalamud.Utility; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; @@ -32,13 +33,14 @@ public class AutoTranslatePayload : Payload, ITextProvider this.Group = group; this.Key = key; - var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get(); - this.payload = ssb.BeginMacro(MacroCode.Fixed) - .AppendUIntExpression(group - 1) - .AppendUIntExpression(key) - .EndMacro() - .ToReadOnlySeString(); - Lumina.Text.SeStringBuilder.SharedPool.Return(ssb); + using var rssb = new RentedSeStringBuilder(); + + this.payload = rssb.Builder + .BeginMacro(MacroCode.Fixed) + .AppendUIntExpression(group - 1) + .AppendUIntExpression(key) + .EndMacro() + .ToReadOnlySeString(); } /// diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs index 8b020b111..2becb815b 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs @@ -1,5 +1,7 @@ using System.IO; +using Dalamud.Utility; + using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; @@ -37,19 +39,18 @@ public class DalamudLinkPayload : Payload /// protected override byte[] EncodeImpl() { - var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get(); - var res = ssb.BeginMacro(MacroCode.Link) - .AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1) - .AppendUIntExpression(this.CommandId) - .AppendIntExpression(this.Extra1) - .AppendIntExpression(this.Extra2) - .BeginStringExpression() - .Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString })) - .EndExpression() - .EndMacro() - .ToArray(); - Lumina.Text.SeStringBuilder.SharedPool.Return(ssb); - return res; + using var rssb = new RentedSeStringBuilder(); + return rssb.Builder + .BeginMacro(MacroCode.Link) + .AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1) + .AppendUIntExpression(this.CommandId) + .AppendIntExpression(this.Extra1) + .AppendIntExpression(this.Extra2) + .BeginStringExpression() + .Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString })) + .EndExpression() + .EndMacro() + .ToArray(); } /// diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs index 55697782e..01ca1b955 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; using System.IO; -using System.Text; using Dalamud.Data; +using Dalamud.Utility; using Lumina.Excel; using Lumina.Excel.Sheets; @@ -87,14 +86,12 @@ public class PlayerPayload : Payload /// protected override byte[] EncodeImpl() { - var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get(); - var res = ssb - .PushLinkCharacter(this.playerName, this.serverId) - .Append(this.playerName) - .PopLink() - .ToArray(); - Lumina.Text.SeStringBuilder.SharedPool.Return(ssb); - return res; + using var rssb = new RentedSeStringBuilder(); + return rssb.Builder + .PushLinkCharacter(this.playerName, this.serverId) + .Append(this.playerName) + .PopLink() + .ToArray(); } /// diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index a1ef5e936..ca14299db 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -198,8 +198,9 @@ public class SeString var textColor = ItemUtil.GetItemRarityColorType(rawId); var textEdgeColor = textColor + 1u; - var sb = LSeStringBuilder.SharedPool.Get(); - var itemLink = sb + using var rssb = new RentedSeStringBuilder(); + + var itemLink = rssb.Builder .PushColorType(textColor) .PushEdgeColorType(textEdgeColor) .PushLinkItem(rawId, copyName) @@ -208,7 +209,6 @@ public class SeString .PopEdgeColorType() .PopColorType() .ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(sb); return SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage)); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index e9b4022e4..d43d3b7b2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -29,8 +29,6 @@ using Lumina.Text.Expressions; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; - namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// @@ -474,25 +472,25 @@ internal class SeStringCreatorWidget : IDataWindowWidget if (ImGui.Button("Print Evaluated"u8)) { - var sb = new LSeStringBuilder(); + using var rssb = new RentedSeStringBuilder(); foreach (var entry in this.entries) { switch (entry.Type) { case TextEntryType.String: - sb.Append(entry.Message); + rssb.Builder.Append(entry.Message); break; case TextEntryType.Macro: case TextEntryType.Fixed: - sb.AppendMacroString(entry.Message); + rssb.Builder.AppendMacroString(entry.Message); break; } } var evaluated = Service.Get().Evaluate( - sb.ToReadOnlySeString(), + rssb.Builder.ToReadOnlySeString(), this.localParameters, this.language); @@ -505,24 +503,24 @@ internal class SeStringCreatorWidget : IDataWindowWidget if (ImGui.Button("Copy MacroString"u8)) { - var sb = new LSeStringBuilder(); + using var rssb = new RentedSeStringBuilder(); foreach (var entry in this.entries) { switch (entry.Type) { case TextEntryType.String: - sb.Append(entry.Message); + rssb.Builder.Append(entry.Message); break; case TextEntryType.Macro: case TextEntryType.Fixed: - sb.AppendMacroString(entry.Message); + rssb.Builder.AppendMacroString(entry.Message); break; } } - ImGui.SetClipboardText(sb.ToReadOnlySeString().ToMacroString()); + ImGui.SetClipboardText(rssb.Builder.ToReadOnlySeString().ToMacroString()); } ImGui.SameLine(); @@ -802,24 +800,24 @@ internal class SeStringCreatorWidget : IDataWindowWidget private unsafe void UpdateInputString(bool resetLocalParameters = true) { - var sb = new LSeStringBuilder(); + using var rssb = new RentedSeStringBuilder(); foreach (var entry in this.entries) { switch (entry.Type) { case TextEntryType.String: - sb.Append(entry.Message); + rssb.Builder.Append(entry.Message); break; case TextEntryType.Macro: case TextEntryType.Fixed: - sb.AppendMacroString(entry.Message); + rssb.Builder.AppendMacroString(entry.Message); break; } } - this.input = sb.ToReadOnlySeString(); + this.input = rssb.Builder.ToReadOnlySeString(); if (resetLocalParameters) this.localParameters = null; @@ -998,10 +996,9 @@ internal class SeStringCreatorWidget : IDataWindowWidget } } - var builder = LSeStringBuilder.SharedPool.Get(); - builder.AppendIcon(iconId); - ImGuiHelpers.SeStringWrapped(builder.ToArray()); - LSeStringBuilder.SharedPool.Return(builder); + using var rssb = new RentedSeStringBuilder(); + rssb.Builder.AppendIcon(iconId); + ImGuiHelpers.SeStringWrapped(rssb.Builder.ToArray()); ImGui.SameLine(); } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs index d272032e7..e2ee676df 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs @@ -3,9 +3,9 @@ using System.Linq; using Dalamud.Game.ClientState.GamePad; using Dalamud.Interface.Utility; using Dalamud.Plugin.SelfTest; -using Lumina.Text.Payloads; +using Dalamud.Utility; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; +using Lumina.Text.Payloads; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; @@ -29,25 +29,25 @@ internal class GamepadStateSelfTestStep : ISelfTestStep (GamepadButtons.L1, 12), }; - var builder = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - builder.Append("Hold down "); + rssb.Builder.Append("Hold down "); for (var i = 0; i < buttons.Length; i++) { var (button, iconId) = buttons[i]; - builder.BeginMacro(MacroCode.Icon).AppendUIntExpression(iconId).EndMacro(); - builder.PushColorRgba(gamepadState.Raw(button) == 1 ? 0x0000FF00u : 0x000000FF); - builder.Append(button.ToString()); - builder.PopColor(); - - builder.Append(i < buttons.Length - 1 ? ", " : "."); + rssb.Builder + .BeginMacro(MacroCode.Icon) + .AppendUIntExpression(iconId) + .EndMacro() + .PushColorRgba(gamepadState.Raw(button) == 1 ? 0x0000FF00u : 0x000000FF) + .Append(button.ToString()) + .PopColor() + .Append(i < buttons.Length - 1 ? ", " : "."); } - ImGuiHelpers.SeStringWrapped(builder.ToReadOnlySeString()); - - LSeStringBuilder.SharedPool.Return(builder); + ImGuiHelpers.SeStringWrapped(rssb.Builder.ToReadOnlySeString()); if (buttons.All(tuple => gamepadState.Raw(tuple.Button) == 1)) { diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index f826da622..ec9440e0e 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -26,8 +26,6 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.ReadOnly; using Serilog; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; - namespace Dalamud.Interface.Internal.Windows; /// @@ -498,20 +496,23 @@ internal class TitleScreenMenuWindow : Window, IDisposable return; this.lastLoadedPluginCount = count; - var lssb = LSeStringBuilder.SharedPool.Get(); - lssb.Append(new ReadOnlySeStringSpan(addon->AtkValues[1].String.Value)).Append("\n\n"); - lssb.PushEdgeColorType(701).PushColorType(539) - .Append(SeIconChar.BoxedLetterD.ToIconChar()) - .PopColorType().PopEdgeColorType(); - lssb.Append($" Dalamud: {Versioning.GetScmVersion()}"); + using var rssb = new RentedSeStringBuilder(); - lssb.Append($" - {count} {(count != 1 ? "plugins" : "plugin")} loaded"); + rssb.Builder + .Append(new ReadOnlySeStringSpan(addon->AtkValues[1].String.Value)) + .Append("\n\n") + .PushEdgeColorType(701) + .PushColorType(539) + .Append(SeIconChar.BoxedLetterD.ToIconChar()) + .PopColorType() + .PopEdgeColorType() + .Append($" Dalamud: {Versioning.GetScmVersion()}") + .Append($" - {count} {(count != 1 ? "plugins" : "plugin")} loaded"); if (pm?.SafeMode is true) - lssb.PushColorType(17).Append(" [SAFE MODE]").PopColorType(); + rssb.Builder.PushColorType(17).Append(" [SAFE MODE]").PopColorType(); - textNode->SetText(lssb.GetViewAsSpan()); - LSeStringBuilder.SharedPool.Return(lssb); + textNode->SetText(rssb.Builder.GetViewAsSpan()); } private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync(); diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs index b632d14d7..8ee465486 100644 --- a/Dalamud/Utility/ItemUtil.cs +++ b/Dalamud/Utility/ItemUtil.cs @@ -5,7 +5,6 @@ using Dalamud.Game; using Dalamud.Game.Text; using Lumina.Excel.Sheets; -using Lumina.Text; using Lumina.Text.ReadOnly; namespace Dalamud.Utility; @@ -150,23 +149,21 @@ public static class ItemUtil if (!includeIcon || kind is not (ItemKind.Hq or ItemKind.Collectible)) return item.Name; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - builder.Append(item.Name); + rssb.Builder.Append(item.Name); switch (kind) { case ItemKind.Hq: - builder.Append($" {(char)SeIconChar.HighQuality}"); + rssb.Builder.Append($" {(char)SeIconChar.HighQuality}"); break; case ItemKind.Collectible: - builder.Append($" {(char)SeIconChar.Collectible}"); + rssb.Builder.Append($" {(char)SeIconChar.Collectible}"); break; } - var itemName = builder.ToReadOnlySeString(); - SeStringBuilder.SharedPool.Return(builder); - return itemName; + return rssb.Builder.ToReadOnlySeString(); } /// diff --git a/Dalamud/Utility/SeStringExtensions.cs b/Dalamud/Utility/SeStringExtensions.cs index cd095c467..9de116f26 100644 --- a/Dalamud/Utility/SeStringExtensions.cs +++ b/Dalamud/Utility/SeStringExtensions.cs @@ -1,7 +1,3 @@ -using System.Linq; - -using InteropGenerator.Runtime; - using Lumina.Text.Parse; using Lumina.Text.ReadOnly; @@ -49,11 +45,9 @@ public static class SeStringExtensions /// this for method chaining. public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan macroString) { - var lssb = LSeStringBuilder.SharedPool.Get(); - lssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); - ssb.Append(DSeString.Parse(lssb.ToReadOnlySeString().Data.Span)); - LSeStringBuilder.SharedPool.Return(lssb); - return ssb; + using var rssb = new RentedSeStringBuilder(); + rssb.Builder.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); + return ssb.Append(DSeString.Parse(rssb.Builder.ToReadOnlySeString().Data.Span)); } /// Compiles and appends a macro string. @@ -62,11 +56,9 @@ public static class SeStringExtensions /// this for method chaining. public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan macroString) { - var lssb = LSeStringBuilder.SharedPool.Get(); - lssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); - ssb.Append(DSeString.Parse(lssb.ToReadOnlySeString().Data.Span)); - LSeStringBuilder.SharedPool.Return(lssb); - return ssb; + using var rssb = new RentedSeStringBuilder(); + rssb.Builder.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); + return ssb.Append(DSeString.Parse(rssb.Builder.ToReadOnlySeString().Data.Span)); } /// @@ -163,7 +155,7 @@ public static class SeStringExtensions if (ross.IsEmpty) return ross; - var sb = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); foreach (var payload in ross) { @@ -172,25 +164,25 @@ public static class SeStringExtensions if (payload.Type != ReadOnlySePayloadType.Text) { - sb.Append(payload); + rssb.Builder.Append(payload); continue; } var index = payload.Body.Span.IndexOf(toFind); if (index == -1) { - sb.Append(payload); + rssb.Builder.Append(payload); continue; } var lastIndex = 0; while (index != -1) { - sb.Append(payload.Body.Span[lastIndex..index]); + rssb.Builder.Append(payload.Body.Span[lastIndex..index]); if (!replacement.IsEmpty) { - sb.Append(replacement); + rssb.Builder.Append(replacement); } lastIndex = index + toFind.Length; @@ -200,12 +192,10 @@ public static class SeStringExtensions index += lastIndex; } - sb.Append(payload.Body.Span[lastIndex..]); + rssb.Builder.Append(payload.Body.Span[lastIndex..]); } - var output = sb.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(sb); - return output; + return rssb.Builder.ToReadOnlySeString(); } /// From 8a49a5ee484177e83ade8078486ee7b5f93d924e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 19 Dec 2025 17:39:31 +0100 Subject: [PATCH 350/477] Use spread element for TextArrowPayloads Also changes CreatePartyFinderLink to use client-language-based text. --- .../Game/Text/SeStringHandling/SeString.cs | 54 +++++++------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index ca14299db..b94b05cac 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -250,16 +250,12 @@ public class SeString var mapPayload = new MapLinkPayload(territoryId, mapId, rawX, rawY); var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString); - var payloads = new List(new Payload[] - { + return new SeString(new List([ mapPayload, - // arrow goes here + ..TextArrowPayloads, new TextPayload(nameString), RawPayload.LinkTerminator, - }); - payloads.InsertRange(1, TextArrowPayloads); - - return new SeString(payloads); + ])); } /// @@ -290,16 +286,12 @@ public class SeString var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor); var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString); - var payloads = new List(new Payload[] - { + return new SeString(new List([ mapPayload, - // arrow goes here + ..TextArrowPayloads, new TextPayload(nameString), RawPayload.LinkTerminator, - }); - payloads.InsertRange(1, TextArrowPayloads); - - return new SeString(payloads); + ])); } /// @@ -355,21 +347,15 @@ public class SeString /// An SeString containing all the payloads necessary to display a party finder link in the chat log. public static SeString CreatePartyFinderLink(uint listingId, string recruiterName, bool isCrossWorld = false) { - var payloads = new List() - { + var clientState = Service.Get(); + var seStringEvaluator = Service.Get(); + + return new SeString(new List([ new PartyFinderPayload(listingId, isCrossWorld ? PartyFinderPayload.PartyFinderLinkType.NotSpecified : PartyFinderPayload.PartyFinderLinkType.LimitedToHomeWorld), - // -> - new TextPayload($"Looking for Party ({recruiterName})" + (isCrossWorld ? " " : string.Empty)), - }; - - payloads.InsertRange(1, TextArrowPayloads); - - if (isCrossWorld) - payloads.Add(new IconPayload(BitmapFontIcon.CrossWorld)); - - payloads.Add(RawPayload.LinkTerminator); - - return new SeString(payloads); + ..TextArrowPayloads, + ..SeString.Parse(seStringEvaluator.EvaluateFromAddon(2265, [recruiterName, isCrossWorld ? 0 : 1], clientState.ClientLanguage)).Payloads, + RawPayload.LinkTerminator + ])); } /// @@ -379,16 +365,12 @@ public class SeString /// An SeString containing all the payloads necessary to display a link to the party finder search conditions. public static SeString CreatePartyFinderSearchConditionsLink(string message) { - var payloads = new List() - { + return new SeString(new List([ new PartyFinderPayload(), - // -> + ..TextArrowPayloads, new TextPayload(message), - }; - payloads.InsertRange(1, TextArrowPayloads); - payloads.Add(RawPayload.LinkTerminator); - - return new SeString(payloads); + RawPayload.LinkTerminator + ])); } /// From efed9ca20b74452bf332c9cd4f68f5588bb81156 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Dec 2025 20:55:30 +0100 Subject: [PATCH 351/477] Add badges --- .../Internal/DalamudConfiguration.cs | 10 ++ Dalamud/DalamudAsset.cs | 9 +- Dalamud/Interface/DalamudWindowOpenKinds.cs | 5 + Dalamud/Interface/Internal/Badge/BadgeInfo.cs | 48 +++++++ .../Interface/Internal/Badge/BadgeManager.cs | 85 ++++++++++++ .../Internal/Badge/BadgeUnlockMethod.cs | 22 +++ .../Interface/Internal/DalamudInterface.cs | 85 ++++++++++++ .../Windows/Settings/SettingsWindow.cs | 1 + .../Windows/Settings/Tabs/SettingsTabBadge.cs | 128 ++++++++++++++++++ 9 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Interface/Internal/Badge/BadgeInfo.cs create mode 100644 Dalamud/Interface/Internal/Badge/BadgeManager.cs create mode 100644 Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs create mode 100644 Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index d546dc517..ddcb26914 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -495,6 +495,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService #pragma warning restore SA1516 #pragma warning restore SA1600 + /// + /// Gets or sets a list of badge passwords used to unlock badges. + /// + public List UsedBadgePasswords { get; set; } = []; + + /// + /// Gets or sets a value indicating whether badges should be shown on the title screen. + /// + public bool ShowBadgesOnTitleScreen { get; set; } = true; + /// /// Load a configuration from the provided path. /// diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index e234fbb4c..9c0f247ee 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -73,7 +73,7 @@ public enum DalamudAsset [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "troubleIcon.png")] TroubleIcon = 1006, - + /// /// : The plugin trouble icon overlay. /// @@ -124,6 +124,13 @@ public enum DalamudAsset [DalamudAssetPath("UIRes", "tsmShade.png")] TitleScreenMenuShade = 1013, + /// + /// : Atlas containing badges. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "badgeAtlas.png")] + BadgeAtlas = 1015, + /// /// : Noto Sans CJK JP Medium. /// diff --git a/Dalamud/Interface/DalamudWindowOpenKinds.cs b/Dalamud/Interface/DalamudWindowOpenKinds.cs index 35d2825f7..891f9281a 100644 --- a/Dalamud/Interface/DalamudWindowOpenKinds.cs +++ b/Dalamud/Interface/DalamudWindowOpenKinds.cs @@ -56,6 +56,11 @@ public enum SettingsOpenKind /// ServerInfoBar, + /// + /// Open to the "Badges" page. + /// + Badge, + /// /// Open to the "Experimental" page. /// diff --git a/Dalamud/Interface/Internal/Badge/BadgeInfo.cs b/Dalamud/Interface/Internal/Badge/BadgeInfo.cs new file mode 100644 index 000000000..0787f0658 --- /dev/null +++ b/Dalamud/Interface/Internal/Badge/BadgeInfo.cs @@ -0,0 +1,48 @@ +using System.Numerics; + +namespace Dalamud.Interface.Internal.Badge; + +/// +/// Represents information about a badge. +/// +/// Name of the badge. +/// Description of the badge. +/// Icon index. +/// Sha256 hash of the unlock password. +/// How the badge is unlocked. +internal record BadgeInfo( + Func Name, + Func Description, + int IconIndex, + string UnlockSha256, + BadgeUnlockMethod UnlockMethod) +{ + private const float BadgeWidth = 256; + private const float BadgeHeight = 256; + private const float BadgesPerRow = 2; + + /// + /// Gets the UV coordinates for the badge icon in the atlas. + /// + /// Width of the atlas. + /// Height of the atlas. + /// UV coordinates. + public (Vector2 Uv0, Vector2 Uv1) GetIconUv(float atlasWidthPx, float atlasHeightPx) + { + // Calculate row and column from icon index + var col = this.IconIndex % (int)BadgesPerRow; + var row = this.IconIndex / (int)BadgesPerRow; + + // Calculate pixel positions + var x0 = col * BadgeWidth; + var y0 = row * BadgeHeight; + var x1 = x0 + BadgeWidth; + var y1 = y0 + BadgeHeight; + + // Convert to UV coordinates (0.0 to 1.0) + var uv0 = new Vector2(x0 / atlasWidthPx, y0 / atlasHeightPx); + var uv1 = new Vector2(x1 / atlasWidthPx, y1 / atlasHeightPx); + + return (uv0, uv1); + } +} diff --git a/Dalamud/Interface/Internal/Badge/BadgeManager.cs b/Dalamud/Interface/Internal/Badge/BadgeManager.cs new file mode 100644 index 000000000..9290d6cc8 --- /dev/null +++ b/Dalamud/Interface/Internal/Badge/BadgeManager.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Configuration.Internal; + +namespace Dalamud.Interface.Internal.Badge; + +/// +/// Service responsible for managing user badges. +/// +[ServiceManager.EarlyLoadedService] +internal class BadgeManager : IServiceType +{ + private readonly DalamudConfiguration configuration; + + private readonly List badges = + [ + new(() => "Test Badge", + () => "Awarded for testing badges.", + 0, + "937e8d5fbb48bd4949536cd65b8d35c426b80d2f830c5c308e2cdec422ae2244", + BadgeUnlockMethod.User), + + new(() => "Fundraiser #1 Donor", + () => "Awarded for participating in the first patch fundraiser.", + 1, + "56e752257bd0cbb2944f95cc7b3cb3d0db15091dd043f7a195ed37028d079322", + BadgeUnlockMethod.User) + ]; + + private readonly List unlockedBadgeIndices = []; + + /// + /// Initializes a new instance of the class. + /// + /// Configuration to use. + [ServiceManager.ServiceConstructor] + public BadgeManager(DalamudConfiguration configuration) + { + this.configuration = configuration; + + foreach (var usedBadge in this.configuration.UsedBadgePasswords) + { + this.TryUnlockBadge(usedBadge, BadgeUnlockMethod.Startup, out _); + } + } + + /// + /// Gets the badges the user has unlocked. + /// + public IEnumerable UnlockedBadges + => this.badges.Where((_, index) => this.unlockedBadgeIndices.Contains(index)); + + /// + /// Unlock a badge with the given password and method. + /// + /// The password to unlock the badge with. + /// How we are unlocking this badge. + /// The badge that was unlocked, if the function returns true, null otherwise. + /// The unlocked badge, if one was unlocked by this call. + public bool TryUnlockBadge(string password, BadgeUnlockMethod method, out BadgeInfo unlockedBadge) + { + var sha256 = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(password)); + var hashString = Convert.ToHexString(sha256); + + foreach (var (idx, badge) in this.badges.Where(x => x.UnlockMethod == method || method == BadgeUnlockMethod.Startup).Index()) + { + if (!this.unlockedBadgeIndices.Contains(idx) && badge.UnlockSha256.Equals(hashString, StringComparison.OrdinalIgnoreCase)) + { + if (method != BadgeUnlockMethod.Startup) + { + this.configuration.UsedBadgePasswords.Add(password); + this.configuration.QueueSave(); + } + + this.unlockedBadgeIndices.Add(idx); + unlockedBadge = badge; + return true; + } + } + + unlockedBadge = null!; + return false; + } +} diff --git a/Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs b/Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs new file mode 100644 index 000000000..45828c097 --- /dev/null +++ b/Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.Internal.Badge; + +/// +/// Method by which a badge can be unlocked. +/// +internal enum BadgeUnlockMethod +{ + /// + /// Badge can be unlocked by the user by entering a password. + /// + User, + + /// + /// Badge can be unlocked from Dalamud internal features. + /// + Internal, + + /// + /// Badge is no longer obtainable and can only be unlocked from the configuration file. + /// + Startup, +} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index be4228a81..13e7ff3f7 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -19,6 +19,9 @@ using Dalamud.Game.Gui; using Dalamud.Hooking; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal.Badge; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.Data; using Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -540,6 +543,25 @@ internal class DalamudInterface : IInternalDisposableService /// Widget to set current. public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget; + /// + /// Play an animation when a badge has been unlocked. + /// + /// The badge that has been unlocked. + public void StartBadgeUnlockAnimation(BadgeInfo badge) + { + var badgeTexture = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); + var uvs = badge.GetIconUv(badgeTexture.Width, badgeTexture.Height); + + // TODO: Make it more fancy? + Service.Get().AddNotification( + new Notification + { + Title = "Badge unlocked!", + Content = $"You unlocked the badge '{badge.Name()}'", + Type = NotificationType.Success, + }); + } + private void OnDraw() { this.FrameCount++; @@ -561,6 +583,7 @@ internal class DalamudInterface : IInternalDisposableService { this.DrawHiddenDevMenuOpener(); this.DrawDevMenu(); + this.DrawTitleScreenBadges(); if (Service.Get().GameUiHidden) return; @@ -591,6 +614,68 @@ internal class DalamudInterface : IInternalDisposableService } } + private void DrawTitleScreenBadges() + { + if (!this.titleScreenMenuWindow.IsOpen) + return; + + var badgeManager = Service.Get(); + if (!this.configuration.ShowBadgesOnTitleScreen || !badgeManager.UnlockedBadges.Any()) + return; + + var vp = ImGui.GetMainViewport(); + ImGui.SetNextWindowPos(vp.Pos); + ImGui.SetNextWindowSize(vp.Size); + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowBgAlpha(0f); + + ImGui.Begin( + "###TitleScreenBadgeWindow"u8, + ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.NoNav); + + var badgeAtlas = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); + var badgeSize = ImGuiHelpers.GlobalScale * 80; + var spacing = ImGuiHelpers.GlobalScale * 10; + const float margin = 60f; + var startPos = vp.Pos + new Vector2(vp.Size.X - margin, margin); + + // Use the mouse position in screen space for hover detection because the usual ImGui hover checks + // don't work with this full-viewport overlay window setup. + var mouse = ImGui.GetMousePos(); + + foreach (var badge in badgeManager.UnlockedBadges) + { + var uvs = badge.GetIconUv(badgeAtlas.Width, badgeAtlas.Height); + + startPos.X -= badgeSize; + ImGui.SetCursorPos(startPos); + ImGui.Image(badgeAtlas.Handle, new Vector2(badgeSize), uvs.Uv0, uvs.Uv1); + + // Get the actual screen-space bounds of the image we just drew + var badgeMin = ImGui.GetItemRectMin(); + var badgeMax = ImGui.GetItemRectMax(); + + // add spacing to the left for the next badge + startPos.X -= spacing; + + // Manual hit test using mouse position + if (mouse.X >= badgeMin.X && mouse.X <= badgeMax.X && mouse.Y >= badgeMin.Y && mouse.Y <= badgeMax.Y) + { + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(300 * ImGuiHelpers.GlobalScale); + ImGui.TextWrapped(badge.Name()); + ImGui.Separator(); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, badge.Description()); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + } + + ImGui.End(); + } + private void DrawCreditsDarkeningAnimation() { using var style1 = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f); diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 581ef3746..62c931b20 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -46,6 +46,7 @@ internal sealed class SettingsWindow : Window new SettingsTabLook(), new SettingsTabAutoUpdates(), new SettingsTabDtr(), + new SettingsTabBadge(), new SettingsTabExperimental(), new SettingsTabAbout() ]; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs new file mode 100644 index 000000000..8e44ef7ea --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs @@ -0,0 +1,128 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using CheapLoc; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.Badge; +using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.Utility; +using Dalamud.Storage.Assets; +using Dalamud.Utility.Internal; + +namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; + +[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] +internal sealed class SettingsTabBadge : SettingsTab +{ + private string badgePassword = string.Empty; + private bool badgeWasError = false; + + public override string Title => Loc.Localize("DalamudSettingsBadge", "Badges"); + + public override SettingsOpenKind Kind => SettingsOpenKind.ServerInfoBar; + + public override SettingsEntry[] Entries { get; } = + [ + new SettingsEntry( + LazyLoc.Localize("DalamudSettingsShowBadgesOnTitleScreen", "Show Badges on Title Screen"), + LazyLoc.Localize("DalamudSettingsShowBadgesOnTitleScreenHint", "If enabled, your unlocked badges will also be shown on the title screen."), + c => c.ShowBadgesOnTitleScreen, + (v, c) => c.ShowBadgesOnTitleScreen = v), + ]; + + public override void Draw() + { + var badgeManager = Service.Get(); + var dalamudInterface = Service.Get(); + + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarHint", "Plugins can put additional information into your server information bar(where world & time can be seen).\nYou can reorder and disable these here.")); + + ImGuiHelpers.ScaledDummy(5); + + ImGui.Text(Loc.Localize("DalamudSettingsBadgesUnlock", "Unlock a badge")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsBadgesUnlockHint", "If you have received a code for a badge, enter it here to unlock the badge.\nCodes are usually given out during community events or contests.")); + ImGui.InputTextWithHint( + "##BadgePassword", + Loc.Localize("DalamudSettingsBadgesUnlockHintInput", "Enter badge code here"), + ref this.badgePassword, + 100); + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("DalamudSettingsBadgesUnlockButton", "Unlock Badge"))) + { + if (badgeManager.TryUnlockBadge(this.badgePassword.Trim(), BadgeUnlockMethod.User, out var unlockedBadge)) + { + dalamudInterface.StartBadgeUnlockAnimation(unlockedBadge); + this.badgeWasError = false; + } + else + { + this.badgeWasError = true; + } + } + + if (this.badgeWasError) + { + ImGuiHelpers.ScaledDummy(5); + ImGui.TextColored(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingsBadgesUnlockError", "Failed to unlock badge. The code may be invalid or you may have already unlocked this badge.")); + } + + ImGuiHelpers.ScaledDummy(5); + + base.Draw(); + + ImGui.Separator(); + + ImGuiHelpers.ScaledDummy(5); + + var haveBadges = badgeManager.UnlockedBadges.ToArray(); + + if (haveBadges.Length == 0) + { + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarDidNone", "You did not unlock any badges yet.\nBadges can be unlocked by participating in community events or contests.")); + } + + var badgeTexture = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); + foreach (var badge in haveBadges) + { + var uvs = badge.GetIconUv(badgeTexture.Width, badgeTexture.Height); + var sectionSize = ImGuiHelpers.GlobalScale * 66; + + var startCursor = ImGui.GetCursorPos(); + + ImGui.SetCursorPos(startCursor); + + var iconSize = ImGuiHelpers.ScaledVector2(64, 64); + var cursorBeforeImage = ImGui.GetCursorPos(); + var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos(); + + if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize)) + { + ImGui.Image(badgeTexture.Handle, iconSize, uvs.Uv0, uvs.Uv1); + ImGui.SameLine(); + ImGui.SetCursorPos(cursorBeforeImage); + } + + ImGui.SameLine(); + + ImGuiHelpers.ScaledDummy(5); + ImGui.SameLine(); + + var cursor = ImGui.GetCursorPos(); + + // Name + ImGui.Text(badge.Name()); + + cursor.Y += ImGui.GetTextLineHeightWithSpacing(); + ImGui.SetCursorPos(cursor); + + // Description + ImGui.TextWrapped(badge.Description()); + + startCursor.Y += sectionSize; + ImGui.SetCursorPos(startCursor); + + ImGuiHelpers.ScaledDummy(5); + } + } +} From c7dd694a538020d1809fbdffb29629251df5bea1 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 20 Dec 2025 02:02:57 +0100 Subject: [PATCH 352/477] Revert "Prevent ImGui text box methods from cloning unchanged input every frame" This reverts commit db5f27518fb6ec50cdfa5d6dcd39e10cbb4f1fd7. Causes issues with certain flags. --- .../Custom/ImGui.Manual.cs | 153 ++++-------------- 1 file changed, 34 insertions(+), 119 deletions(-) diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs index ce1bf961d..89b3cc3d6 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs @@ -127,13 +127,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -145,13 +140,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -163,13 +153,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -181,13 +166,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, in context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -307,13 +287,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -325,13 +300,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -344,13 +314,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, ref context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -363,13 +328,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, in context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -428,13 +388,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -446,13 +401,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -464,13 +414,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, ref context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -482,13 +427,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, in context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -537,13 +477,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -555,13 +490,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -573,13 +503,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -591,13 +516,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, in context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -621,13 +541,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = TempInputText(bb, id, label, t.Buffer[..(maxLength + 1)], flags); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } From 66fde2d4589516ac5cb687ac102ed6040f94b5d0 Mon Sep 17 00:00:00 2001 From: CMDRNuffin <4348470+CMDRNuffin@users.noreply.github.com> Date: Sat, 20 Dec 2025 03:19:20 +0100 Subject: [PATCH 353/477] Prevent unnecessary string creation in ImGui TextInput methods We now only create a new string if we either know the buffer changed or the EnterReturnsTrue flag was set (because that one does a LOT while still updating the buffer on every actual input), so I had to choose between replicating all that behavior in each of the various InputText methods (hell no, lol), scanning the buffer for actual changes (which would require making another copy) or accepting that in that case we would create a new string every frame. This still makes the GC happy in the majority of cases, while giving callers the option to take a slight performance hit for the convenience EnterReturnsTrue provides. --- .../Custom/ImGui.Manual.cs | 170 ++++++++++++++---- 1 file changed, 136 insertions(+), 34 deletions(-) diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs index 89b3cc3d6..e455e0778 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs @@ -127,8 +127,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -140,8 +146,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -153,8 +165,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -166,8 +184,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -287,8 +311,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -300,8 +330,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -314,8 +350,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -328,8 +370,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -388,8 +436,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -401,8 +455,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -414,8 +474,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -427,8 +493,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -477,8 +549,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -490,8 +568,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -503,8 +587,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -516,8 +606,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -541,8 +637,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = TempInputText(bb, id, label, t.Buffer[..(maxLength + 1)], flags); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } From bc2eac6006aad4c742ed8ffdeb0ec07e8958f2f9 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Fri, 19 Dec 2025 22:18:03 -0800 Subject: [PATCH 354/477] fix: Remove RPC (#2526) --- Dalamud.Test/Rpc/DalamudUriTests.cs | 108 --------- Dalamud/Dalamud.csproj | 1 - Dalamud/Networking/Rpc/Model/DalamudUri.cs | 102 --------- Dalamud/Networking/Rpc/RpcConnection.cs | 95 -------- Dalamud/Networking/Rpc/RpcHostService.cs | 91 -------- Dalamud/Networking/Rpc/RpcServiceRegistry.cs | 85 ------- .../Rpc/Service/ClientHelloService.cs | 133 ----------- .../Rpc/Service/LinkHandlerService.cs | 107 --------- .../Rpc/Service/Links/DebugLinkHandler.cs | 67 ------ .../Rpc/Service/Links/PluginLinkHandler.cs | 57 ----- .../Networking/Rpc/Transport/IRpcTransport.cs | 32 --- .../Rpc/Transport/UnixRpcTransport.cs | 207 ------------------ Dalamud/Plugin/Services/IPluginLinkHandler.cs | 24 -- Directory.Packages.props | 3 - 14 files changed, 1112 deletions(-) delete mode 100644 Dalamud.Test/Rpc/DalamudUriTests.cs delete mode 100644 Dalamud/Networking/Rpc/Model/DalamudUri.cs delete mode 100644 Dalamud/Networking/Rpc/RpcConnection.cs delete mode 100644 Dalamud/Networking/Rpc/RpcHostService.cs delete mode 100644 Dalamud/Networking/Rpc/RpcServiceRegistry.cs delete mode 100644 Dalamud/Networking/Rpc/Service/ClientHelloService.cs delete mode 100644 Dalamud/Networking/Rpc/Service/LinkHandlerService.cs delete mode 100644 Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs delete mode 100644 Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs delete mode 100644 Dalamud/Networking/Rpc/Transport/IRpcTransport.cs delete mode 100644 Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs delete mode 100644 Dalamud/Plugin/Services/IPluginLinkHandler.cs diff --git a/Dalamud.Test/Rpc/DalamudUriTests.cs b/Dalamud.Test/Rpc/DalamudUriTests.cs deleted file mode 100644 index b371a5698..000000000 --- a/Dalamud.Test/Rpc/DalamudUriTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Linq; - -using Dalamud.Networking.Rpc.Model; - -using Xunit; - -namespace Dalamud.Test.Rpc -{ - public class DalamudUriTests - { - [Theory] - [InlineData("https://www.google.com/", false)] - [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)] - public void ValidatesScheme(string uri, bool valid) - { - Action act = () => { _ = DalamudUri.FromUri(uri); }; - - var ex = Record.Exception(act); - if (valid) - { - Assert.Null(ex); - } - else - { - Assert.NotNull(ex); - Assert.IsType(ex); - } - } - - [Theory] - [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")] - [InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")] - [InlineData("dalamud://Test", "test")] - public void ExtractsNamespace(string uri, string expectedNamespace) - { - var dalamudUri = DalamudUri.FromUri(uri); - Assert.Equal(expectedNamespace, dalamudUri.Namespace); - } - - [Theory] - [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")] - [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")] - [InlineData("dalamud://foo/bar/baz", "/bar/baz")] - [InlineData("dalamud://foo/bar", "/bar")] - [InlineData("dalamud://foo/bar/", "/bar/")] - [InlineData("dalamud://foo/", "/")] - public void ExtractsPath(string uri, string expectedPath) - { - var dalamudUri = DalamudUri.FromUri(uri); - Assert.Equal(expectedPath, dalamudUri.Path); - } - - [Theory] - [InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")] - [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")] - [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")] - [InlineData("dalamud://foo/bar/baz", "/bar/baz")] - [InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")] - [InlineData("dalamud://foo/bar", "/bar")] - [InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")] - [InlineData("dalamud://foo/bar/", "/bar/")] - [InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")] - [InlineData("dalamud://foo/?cow=moo", "/?cow=moo")] - [InlineData("dalamud://foo/", "/")] - public void ExtractsData(string uri, string expectedData) - { - var dalamudUri = DalamudUri.FromUri(uri); - - Assert.Equal(expectedData, dalamudUri.Data); - } - - [Theory] - [InlineData("dalamud://foo/bar", 0)] - [InlineData("dalamud://foo/bar?cow=moo", 1)] - [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)] - [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)] - public void ExtractsQueryParams(string uri, int queryCount) - { - var dalamudUri = DalamudUri.FromUri(uri); - Assert.Equal(queryCount, dalamudUri.QueryParams.Count); - } - - [Theory] - [InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)] - [InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)] - [InlineData("dalamud://foo/bar/baz/qux/meh", 5)] - [InlineData("dalamud://foo/bar/baz/qux", 4)] - [InlineData("dalamud://foo/bar/baz", 3)] - [InlineData("dalamud://foo/bar/", 2)] - [InlineData("dalamud://foo/bar", 2)] - public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false) - { - var dalamudUri = DalamudUri.FromUri(uri); - var segments = dalamudUri.Segments; - - // First segment must always be `/` - Assert.Equal("/", segments[0]); - - Assert.Equal(segmentCount, segments.Length); - - if (finalSegmentEndsWithSlash) - { - Assert.EndsWith("/", segments.Last()); - } - } - } -} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 9685b92ac..a2538ebd4 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -79,7 +79,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/Networking/Rpc/Model/DalamudUri.cs b/Dalamud/Networking/Rpc/Model/DalamudUri.cs deleted file mode 100644 index 852478762..000000000 --- a/Dalamud/Networking/Rpc/Model/DalamudUri.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Web; - -namespace Dalamud.Networking.Rpc.Model; - -/// -/// A Dalamud Uri, in the format: -/// dalamud://{NAMESPACE}/{ARBITRARY} -/// -public record DalamudUri -{ - private readonly Uri rawUri; - - private DalamudUri(Uri uri) - { - if (uri.Scheme != "dalamud") - { - throw new ArgumentOutOfRangeException(nameof(uri), "URI must be of scheme dalamud."); - } - - this.rawUri = uri; - } - - /// - /// Gets the namespace that this URI should be routed to. Generally a high level component like "PluginInstaller". - /// - public string Namespace => this.rawUri.Authority; - - /// - /// Gets the raw (untargeted) path and query params for this URI. - /// - public string Data => - this.rawUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped); - - /// - /// Gets the raw (untargeted) path for this URI. - /// - public string Path => this.rawUri.AbsolutePath; - - /// - /// Gets a list of segments based on the provided Data element. - /// - public string[] Segments => this.GetDataSegments(); - - /// - /// Gets the raw query parameters for this URI, if any. - /// - public string Query => this.rawUri.Query; - - /// - /// Gets the query params (as a parsed NameValueCollection) in this URI. - /// - public NameValueCollection QueryParams => HttpUtility.ParseQueryString(this.Query); - - /// - /// Gets the fragment (if one is specified) in this URI. - /// - public string Fragment => this.rawUri.Fragment; - - /// - public override string ToString() => this.rawUri.ToString(); - - /// - /// Build a DalamudURI from a given URI. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(Uri uri) - { - return new DalamudUri(uri); - } - - /// - /// Build a DalamudURI from a URI in string format. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); - - private string[] GetDataSegments() - { - // reimplementation of the System.URI#Segments, under MIT license. - var path = this.Path; - - var segments = new List(); - var current = 0; - while (current < path.Length) - { - var next = path.IndexOf('/', current); - if (next == -1) - { - next = path.Length - 1; - } - - segments.Add(path.Substring(current, (next - current) + 1)); - current = next + 1; - } - - return segments.ToArray(); - } -} diff --git a/Dalamud/Networking/Rpc/RpcConnection.cs b/Dalamud/Networking/Rpc/RpcConnection.cs deleted file mode 100644 index 5288948eb..000000000 --- a/Dalamud/Networking/Rpc/RpcConnection.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Networking.Rpc.Service; - -using Serilog; - -using StreamJsonRpc; - -namespace Dalamud.Networking.Rpc; - -/// -/// A single RPC client session connected via a stream (named pipe or Unix socket). -/// -internal class RpcConnection : IDisposable -{ - private readonly Stream stream; - private readonly RpcServiceRegistry registry; - private readonly CancellationTokenSource cts = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The stream that this connection will handle. - /// A registry of RPC services. - public RpcConnection(Stream stream, RpcServiceRegistry registry) - { - this.Id = Guid.CreateVersion7(); - this.stream = stream; - this.registry = registry; - - var formatter = new JsonMessageFormatter(); - var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter); - - this.Rpc = new JsonRpc(handler); - this.Rpc.AllowModificationWhileListening = true; - this.Rpc.Disconnected += this.OnDisconnected; - this.registry.Attach(this.Rpc); - - this.Rpc.StartListening(); - } - - /// - /// Gets the GUID for this connection. - /// - public Guid Id { get; } - - /// - /// Gets the JsonRpc instance for this connection. - /// - public JsonRpc Rpc { get; } - - /// - /// Gets a task that's called on RPC completion. - /// - public Task Completion => this.Rpc.Completion; - - /// - public void Dispose() - { - if (!this.cts.IsCancellationRequested) - { - this.cts.Cancel(); - } - - try - { - this.Rpc.Dispose(); - } - catch (Exception ex) - { - Log.Debug(ex, "Error disposing JsonRpc for client {Id}", this.Id); - } - - try - { - this.stream.Dispose(); - } - catch (Exception ex) - { - Log.Debug(ex, "Error disposing stream for client {Id}", this.Id); - } - - this.cts.Dispose(); - GC.SuppressFinalize(this); - } - - private void OnDisconnected(object? sender, JsonRpcDisconnectedEventArgs e) - { - Log.Debug("RPC client {Id} disconnected: {Reason}", this.Id, e.Description); - this.registry.Detach(this.Rpc); - this.Dispose(); - } -} diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs deleted file mode 100644 index bbe9dc8eb..000000000 --- a/Dalamud/Networking/Rpc/RpcHostService.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; - -using Dalamud.Logging.Internal; -using Dalamud.Networking.Rpc.Transport; - -namespace Dalamud.Networking.Rpc; - -/// -/// The Dalamud service repsonsible for hosting the RPC. -/// -[ServiceManager.EarlyLoadedService] -internal class RpcHostService : IServiceType, IInternalDisposableService -{ - private readonly ModuleLog log = new("RPC"); - private readonly RpcServiceRegistry registry = new(); - private readonly List transports = []; - - /// - /// Initializes a new instance of the class. - /// - [ServiceManager.ServiceConstructor] - public RpcHostService() - { - this.StartUnixTransport(); - - if (this.transports.Count == 0) - { - this.log.Warning("No RPC hosts could be started on this platform"); - } - } - - /// - /// Gets all active RPC transports. - /// - public IReadOnlyList Transports => this.transports; - - /// - /// Add a new service Object to the RPC host. - /// - /// The object to add. - public void AddService(object service) => this.registry.AddService(service); - - /// - /// Add a new standalone method to the RPC host. - /// - /// The method name to add. - /// The handler to add. - public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); - - /// - public void DisposeService() - { - foreach (var host in this.transports) - { - host.Dispose(); - } - - this.transports.Clear(); - } - - /// - public async Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) - { - var clients = this.transports.SelectMany(t => t.Connections).ToImmutableDictionary(); - - if (!clients.TryGetValue(clientId, out var session)) - throw new KeyNotFoundException($"No client {clientId}"); - - return await session.Rpc.InvokeAsync(method, arguments).ConfigureAwait(false); - } - - /// - public async Task BroadcastNotifyAsync(string method, params object[] arguments) - { - await foreach (var transport in this.transports.ToAsyncEnumerable().ConfigureAwait(false)) - { - await transport.BroadcastNotifyAsync(method, arguments).ConfigureAwait(false); - } - } - - private void StartUnixTransport() - { - var transport = new UnixRpcTransport(this.registry); - this.transports.Add(transport); - transport.Start(); - this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath); - } -} diff --git a/Dalamud/Networking/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs deleted file mode 100644 index 6daea14bf..000000000 --- a/Dalamud/Networking/Rpc/RpcServiceRegistry.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Collections.Generic; -using System.Threading; - -using StreamJsonRpc; - -namespace Dalamud.Networking.Rpc; - -/// -/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session. -/// New sessions get all previously registered targets; newly added targets are attached to all active sessions. -/// -internal class RpcServiceRegistry -{ - private readonly Lock sync = new(); - private readonly List targets = []; - private readonly List<(string Name, Delegate Handler)> methods = []; - private readonly List activeRpcs = []; - - /// - /// Registers a new local RPC target object. Its public JSON-RPC methods become callable by clients. - /// Adds to the registry and attaches it to all active RPC sessions. - /// - /// The service instance containing JSON-RPC callable methods to expose. - public void AddService(object service) - { - lock (this.sync) - { - this.targets.Add(service); - foreach (var rpc in this.activeRpcs) - { - rpc.AddLocalRpcTarget(service); - } - } - } - - /// - /// Registers a new standalone JSON-RPC method. - /// - /// The name of the method to add. - /// The handler to add. - public void AddMethod(string name, Delegate handler) - { - lock (this.sync) - { - this.methods.Add((name, handler)); - foreach (var rpc in this.activeRpcs) - { - rpc.AddLocalRpcMethod(name, handler); - } - } - } - - /// - /// Attaches a JsonRpc instance to the registry so it receives all existing service targets. - /// - /// The JsonRpc instance to attach and populate with current targets. - internal void Attach(JsonRpc rpc) - { - lock (this.sync) - { - this.activeRpcs.Add(rpc); - foreach (var t in this.targets) - { - rpc.AddLocalRpcTarget(t); - } - - foreach (var m in this.methods) - { - rpc.AddLocalRpcMethod(m.Name, m.Handler); - } - } - } - - /// - /// Detaches a JsonRpc instance from the registry (e.g. when a client disconnects). - /// - /// The JsonRpc instance being detached. - internal void Detach(JsonRpc rpc) - { - lock (this.sync) - { - this.activeRpcs.Remove(rpc); - } - } -} diff --git a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs deleted file mode 100644 index ae8319f21..000000000 --- a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Diagnostics; -using System.Threading.Tasks; - -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Utility; - -using Lumina.Excel.Sheets; - -namespace Dalamud.Networking.Rpc.Service; - -/// -/// A minimal service to respond with information about this client. -/// -[ServiceManager.EarlyLoadedService] -internal sealed class ClientHelloService : IInternalDisposableService -{ - /// - /// Initializes a new instance of the class. - /// - /// Injected host service. - [ServiceManager.ServiceConstructor] - public ClientHelloService(RpcHostService rpcHostService) - { - rpcHostService.AddMethod("hello", this.HandleHello); - } - - /// - /// Handle a hello request. - /// - /// . - /// Respond with information. - public async Task HandleHello(ClientHelloRequest request) - { - var dalamud = await Service.GetAsync(); - - return new ClientHelloResponse - { - ApiVersion = "1.0", - DalamudVersion = Versioning.GetScmVersion(), - GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", - ProcessId = Environment.ProcessId, - ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(), - ClientState = await this.GetClientIdentifier(), - }; - } - - /// - public void DisposeService() - { - } - - private async Task GetClientIdentifier() - { - var framework = await Service.GetAsync(); - var clientState = await Service.GetAsync(); - var dataManager = await Service.GetAsync(); - - var clientIdentifier = $"FFXIV Process ${Environment.ProcessId}"; - - await framework.RunOnFrameworkThread(() => - { - if (clientState.IsLoggedIn) - { - var player = clientState.LocalPlayer; - if (player != null) - { - var world = dataManager.GetExcelSheet().GetRow(player.HomeWorld.RowId); - clientIdentifier = $"Logged in as {player.Name.TextValue} @ {world.Name.ExtractText()}"; - } - } - else - { - clientIdentifier = "On login screen"; - } - }); - - return clientIdentifier; - } -} - -/// -/// A request from a client to say hello. -/// -internal record ClientHelloRequest -{ - /// - /// Gets the API version this client is expecting. - /// - public string ApiVersion { get; init; } = string.Empty; - - /// - /// Gets the user agent of the client. - /// - public string UserAgent { get; init; } = string.Empty; -} - -/// -/// A response from Dalamud to a hello request. -/// -internal record ClientHelloResponse -{ - /// - /// Gets the API version this server has offered. - /// - public string? ApiVersion { get; init; } - - /// - /// Gets the current Dalamud version. - /// - public string? DalamudVersion { get; init; } - - /// - /// Gets the current game version. - /// - public string? GameVersion { get; init; } - - /// - /// Gets the process ID of this client. - /// - public int? ProcessId { get; init; } - - /// - /// Gets the time this process started. - /// - public long? ProcessStartTime { get; init; } - - /// - /// Gets a state for this client for user display. - /// - public string? ClientState { get; init; } -} diff --git a/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs deleted file mode 100644 index 9fa311ede..000000000 --- a/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; - -using Dalamud.Logging.Internal; -using Dalamud.Networking.Rpc.Model; -using Dalamud.Utility; - -namespace Dalamud.Networking.Rpc.Service; - -/// -/// A service responsible for handling Dalamud URIs and dispatching them accordingly. -/// -[ServiceManager.EarlyLoadedService] -internal class LinkHandlerService : IInternalDisposableService -{ - private readonly ModuleLog log = new("LinkHandler"); - - // key: namespace (e.g. "plugin" or "PluginInstaller") -> list of handlers - private readonly ConcurrentDictionary>> handlers - = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Initializes a new instance of the class. - /// - /// The injected RPC host service. - [ServiceManager.ServiceConstructor] - public LinkHandlerService(RpcHostService rpcHostService) - { - rpcHostService.AddMethod("handleLink", this.HandleLinkCall); - } - - /// - public void DisposeService() - { - } - - /// - /// Register a handler for a namespace. All URIs with this namespace will be dispatched to the handler. - /// - /// The namespace to use for this subscription. - /// The command handler. - public void Register(string ns, Action handler) - { - if (string.IsNullOrWhiteSpace(ns)) - throw new ArgumentNullException(nameof(ns)); - - var list = this.handlers.GetOrAdd(ns, _ => []); - lock (list) - { - list.Add(handler); - } - - this.log.Verbose("Registered handler for {Namespace}", ns); - } - - /// - /// Unregister a handler. - /// - /// The namespace to use for this subscription. - /// The command handler. - public void Unregister(string ns, Action handler) - { - if (string.IsNullOrWhiteSpace(ns)) - return; - - if (!this.handlers.TryGetValue(ns, out var list)) - return; - - list.RemoveAll(x => x == handler); - - if (list.Count == 0) - this.handlers.TryRemove(ns, out _); - - this.log.Verbose("Unregistered handler for {Namespace}", ns); - } - - /// - /// Dispatch a URI to matching handlers. - /// - /// The URI to parse and dispatch. - public void Dispatch(DalamudUri uri) - { - this.log.Information("Received URI: {Uri}", uri.ToString()); - - var ns = uri.Namespace; - if (!this.handlers.TryGetValue(ns, out var actions)) - return; - - foreach (var h in actions) - { - h.InvokeSafely(uri); - } - } - - /// - /// The RPC-invokable link handler. - /// - /// A plain-text URI to parse. - public void HandleLinkCall(string uri) - { - if (string.IsNullOrWhiteSpace(uri)) - return; - - var du = DalamudUri.FromUri(uri); - this.Dispatch(du); - } -} diff --git a/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs deleted file mode 100644 index 269617fc0..000000000 --- a/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Dalamud.Game.Gui.Toast; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.Internal; -using Dalamud.Networking.Rpc.Model; - -namespace Dalamud.Networking.Rpc.Service.Links; - -#if DEBUG - -/// -/// A debug controller for link handling. -/// -[ServiceManager.EarlyLoadedService] -internal sealed class DebugLinkHandler : IInternalDisposableService -{ - private readonly LinkHandlerService linkHandlerService; - - /// - /// Initializes a new instance of the class. - /// - /// Injected LinkHandler. - [ServiceManager.ServiceConstructor] - public DebugLinkHandler(LinkHandlerService linkHandler) - { - this.linkHandlerService = linkHandler; - - this.linkHandlerService.Register("debug", this.HandleLink); - } - - /// - public void DisposeService() - { - this.linkHandlerService.Unregister("debug", this.HandleLink); - } - - private void HandleLink(DalamudUri uri) - { - var action = uri.Path.Split("/").GetValue(1)?.ToString(); - switch (action) - { - case "toast": - this.ShowToast(uri); - break; - case "notification": - this.ShowNotification(uri); - break; - } - } - - private void ShowToast(DalamudUri uri) - { - var message = uri.QueryParams.Get("message") ?? "Hello, world!"; - Service.Get().ShowNormal(message); - } - - private void ShowNotification(DalamudUri uri) - { - Service.Get().AddNotification( - new Notification - { - Title = uri.QueryParams.Get("title"), - Content = uri.QueryParams.Get("content") ?? "Hello, world!", - }); - } -} - -#endif diff --git a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs deleted file mode 100644 index 3b7f18437..000000000 --- a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Linq; - -using Dalamud.Console; -using Dalamud.IoC; -using Dalamud.IoC.Internal; -using Dalamud.Networking.Rpc.Model; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Plugin.Services; - -#pragma warning disable DAL_RPC - -namespace Dalamud.Networking.Rpc.Service.Links; - -/// -[PluginInterface] -[ServiceManager.ScopedService] -[ResolveVia] -public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler -{ - private readonly LinkHandlerService linkHandler; - private readonly LocalPlugin localPlugin; - - /// - /// Initializes a new instance of the class. - /// - /// The plugin to bind this service to. - /// The central link handler. - internal PluginLinkHandler(LocalPlugin localPlugin, LinkHandlerService linkHandler) - { - this.linkHandler = linkHandler; - this.localPlugin = localPlugin; - - this.linkHandler.Register("plugin", this.HandleUri); - } - - /// - public event IPluginLinkHandler.PluginUriReceived? OnUriReceived; - - /// - public void DisposeService() - { - this.OnUriReceived = null; - this.linkHandler.Unregister("plugin", this.HandleUri); - } - - private void HandleUri(DalamudUri uri) - { - var target = uri.Path.Split("/").ElementAtOrDefault(1); - var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName); - if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase)) - { - return; - } - - this.OnUriReceived?.Invoke(uri); - } -} diff --git a/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs deleted file mode 100644 index ad7578eb4..000000000 --- a/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Dalamud.Networking.Rpc.Transport; - -/// -/// Interface for RPC host implementations (named pipes or Unix sockets). -/// -internal interface IRpcTransport : IDisposable -{ - /// - /// Gets a list of active RPC connections. - /// - IReadOnlyDictionary Connections { get; } - - /// Starts accepting client connections. - void Start(); - - /// Invoke an RPC request on a specific client expecting a result. - /// The client ID to invoke. - /// The method to invoke. - /// Any arguments to invoke. - /// An optional return based on the specified RPC. - /// The expected response type. - Task InvokeClientAsync(Guid clientId, string method, params object[] arguments); - - /// Send a notification to all connected clients (no response expected). - /// The method name to broadcast. - /// The arguments to broadcast. - /// Returns a Task when completed. - Task BroadcastNotifyAsync(string method, params object[] arguments); -} diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs deleted file mode 100644 index 17da51444..000000000 --- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Logging.Internal; -using Dalamud.Utility; - -namespace Dalamud.Networking.Rpc.Transport; - -/// -/// Simple multi-client JSON-RPC Unix socket host using StreamJsonRpc. -/// -internal class UnixRpcTransport : IRpcTransport -{ - private readonly ModuleLog log = new("RPC/Transport/UnixSocket"); - - private readonly RpcServiceRegistry registry; - private readonly CancellationTokenSource cts = new(); - private readonly ConcurrentDictionary sessions = new(); - private readonly string? cleanupSocketDirectory; - - private Task? acceptLoopTask; - private Socket? listenSocket; - - /// - /// Initializes a new instance of the class. - /// - /// The RPC service registry to use. - /// The Unix socket directory to use. If null, defaults to Dalamud home directory. - /// The name of the socket to create. - public UnixRpcTransport(RpcServiceRegistry registry, string? socketDirectory = null, string? socketName = null) - { - this.registry = registry; - socketName ??= $"DalamudRPC.{Environment.ProcessId}.sock"; - - if (!socketDirectory.IsNullOrEmpty()) - { - this.SocketPath = Path.Combine(socketDirectory, socketName); - } - else - { - socketDirectory = Service.Get().StartInfo.TempDirectory; - - if (socketDirectory == null) - { - this.SocketPath = Path.Combine(Path.GetTempPath(), socketName); - this.log.Warning("Temp dir was not set in StartInfo; using system temp for unix socket."); - } - else - { - this.SocketPath = Path.Combine(socketDirectory, socketName); - this.cleanupSocketDirectory = socketDirectory; - } - } - } - - /// - /// Gets the path of the Unix socket this RPC host is using. - /// - public string SocketPath { get; } - - /// - public IReadOnlyDictionary Connections => this.sessions; - - /// Starts accepting client connections. - public void Start() - { - if (this.acceptLoopTask != null) return; - - // Make the directory for the socket if it doesn't exist - var socketDir = Path.GetDirectoryName(this.SocketPath); - if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir)) - { - this.log.Error("Directory for unix socket does not exist: {Path}", socketDir); - return; - } - - // Delete existing socket for this PID, if it exists. - if (File.Exists(this.SocketPath)) - { - try - { - File.Delete(this.SocketPath); - } - catch (Exception ex) - { - this.log.Warning(ex, "Failed to delete existing socket file: {Path}", this.SocketPath); - } - } - - this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); - } - - /// Invoke an RPC request on a specific client expecting a result. - /// The client ID to invoke. - /// The method to invoke. - /// Any arguments to invoke. - /// An optional return based on the specified RPC. - /// The expected response type. - public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) - { - if (!this.sessions.TryGetValue(clientId, out var session)) - throw new KeyNotFoundException($"No client {clientId}"); - - return session.Rpc.InvokeAsync(method, arguments); - } - - /// Send a notification to all connected clients (no response expected). - /// The method name to broadcast. - /// The arguments to broadcast. - /// Returns a Task when completed. - public Task BroadcastNotifyAsync(string method, params object[] arguments) - { - var list = this.sessions.Values; - var tasks = new List(list.Count); - foreach (var s in list) - { - tasks.Add(s.Rpc.NotifyAsync(method, arguments)); - } - - return Task.WhenAll(tasks); - } - - /// - public void Dispose() - { - this.cts.Cancel(); - this.acceptLoopTask?.Wait(1000); - - foreach (var kv in this.sessions) - { - kv.Value.Dispose(); - } - - this.sessions.Clear(); - - this.listenSocket?.Dispose(); - - if (File.Exists(this.SocketPath)) - { - try - { - File.Delete(this.SocketPath); - } - catch (Exception ex) - { - this.log.Warning(ex, "Failed to delete socket file on dispose: {Path}", this.SocketPath); - } - } - - this.cts.Dispose(); - this.log.Information("UnixRpcHost disposed ({Socket})", this.SocketPath); - GC.SuppressFinalize(this); - } - - private async Task AcceptLoopAsync() - { - var token = this.cts.Token; - - try - { - var endpoint = new UnixDomainSocketEndPoint(this.SocketPath); - this.listenSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - this.listenSocket.Bind(endpoint); - this.listenSocket.Listen(128); - - while (!token.IsCancellationRequested) - { - Socket? clientSocket = null; - try - { - clientSocket = await this.listenSocket.AcceptAsync(token).ConfigureAwait(false); - - var stream = new NetworkStream(clientSocket, ownsSocket: true); - var session = new RpcConnection(stream, this.registry); - this.sessions.TryAdd(session.Id, session); - - this.log.Debug("RPC connection created: {Id}", session.Id); - - _ = session.Completion.ContinueWith(t => - { - this.sessions.TryRemove(session.Id, out _); - this.log.Debug("RPC connection removed: {Id}", session.Id); - }, TaskScheduler.Default); - } - catch (OperationCanceledException) - { - clientSocket?.Dispose(); - break; - } - catch (Exception ex) - { - clientSocket?.Dispose(); - this.log.Error(ex, "Error in socket accept loop"); - await Task.Delay(500, token).ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - this.log.Error(ex, "Fatal error in Unix socket accept loop"); - } - } -} diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs deleted file mode 100644 index 37101222a..000000000 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -using Dalamud.Networking.Rpc.Model; - -namespace Dalamud.Plugin.Services; - -/// -/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the -/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. -/// -[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] -public interface IPluginLinkHandler : IDalamudService -{ - /// - /// A delegate containing the received URI. - /// - /// The URI opened by the user. - public delegate void PluginUriReceived(DalamudUri uri); - - /// - /// The event fired when a URI targeting this plugin is received. - /// - event PluginUriReceived OnUriReceived; -} diff --git a/Directory.Packages.props b/Directory.Packages.props index 77a4035a4..18760037b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -46,9 +46,6 @@ - - - From 8b0bb343f9e78a167eec64ea9fc06832ac32fed4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 18:33:04 +0000 Subject: [PATCH 355/477] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 7d3f90e61..d6ff8cf46 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 7d3f90e61732df6aef63196d1abaab1074f6f3c9 +Subproject commit d6ff8cf46c7e341989843c28c7550f8d50bee851 From e603af5accdcdd1cba027d3b1182cbd10ab0013a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 18:33:10 +0000 Subject: [PATCH 356/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index faf803a76..2f0f4d2c8 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit faf803a76813511768d45c137a543aaacf5420b8 +Subproject commit 2f0f4d2c86989a7ef04bdbf975d0569948582fc9 From 6374c0d6ae7bdd055075c1e9aa780112a51dc9d8 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Dec 2025 21:25:49 +0100 Subject: [PATCH 357/477] Use UIModuleHandlePacketDetour for ZoneInit --- Dalamud/Game/ClientState/ClientState.cs | 30 ++++++------------- .../ClientState/ClientStateAddressResolver.cs | 6 ---- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 93720e1db..f7c0b75ed 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -37,7 +37,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; - private readonly Hook handleZoneInitPacketHook; private readonly Hook uiModuleHandlePacketHook; private readonly Hook setCurrentInstanceHook; @@ -72,13 +71,11 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language; - this.handleZoneInitPacketHook = Hook.FromAddress(this.AddressResolver.HandleZoneInitPacket, this.HandleZoneInitPacketDetour); this.uiModuleHandlePacketHook = Hook.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour); this.setCurrentInstanceHook = Hook.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour); this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; - this.handleZoneInitPacketHook.Enable(); this.uiModuleHandlePacketHook.Enable(); this.setCurrentInstanceHook.Enable(); @@ -271,7 +268,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState /// void IInternalDisposableService.DisposeService() { - this.handleZoneInitPacketHook.Dispose(); this.uiModuleHandlePacketHook.Dispose(); this.onLogoutHook.Dispose(); this.setCurrentInstanceHook.Dispose(); @@ -294,23 +290,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.framework.Update += this.OnFrameworkUpdate; } - private void HandleZoneInitPacketDetour(nint a1, uint localPlayerEntityId, nint packet, byte type) - { - this.handleZoneInitPacketHook.Original(a1, localPlayerEntityId, packet, type); - - try - { - var eventArgs = ZoneInitEventArgs.Read(packet); - Log.Debug($"ZoneInit: {eventArgs}"); - this.ZoneInit?.InvokeSafely(eventArgs); - this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId; - } - catch (Exception ex) - { - Log.Error(ex, "Exception during ZoneInit"); - } - } - private unsafe void UIModuleHandlePacketDetour( UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet) { @@ -356,6 +335,15 @@ internal sealed class ClientState : IInternalDisposableService, IClientState break; } + + case (UIModulePacketType)5: // TODO: Use UIModulePacketType.InitZone when available + { + var eventArgs = ZoneInitEventArgs.Read((nint)packet); + Log.Debug($"ZoneInit: {eventArgs}"); + this.ZoneInit?.InvokeSafely(eventArgs); + this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId; + break; + } } } diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 53774121d..ae7549b97 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -21,11 +21,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver // Functions - /// - /// Gets the address of the method that handles the ZoneInit packet. - /// - public nint HandleZoneInitPacket { get; private set; } - /// /// Gets the address of the method that sets the current public instance. /// @@ -37,7 +32,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver /// The signature scanner to facilitate setup. protected override void Setup64Bit(ISigScanner sig) { - this.HandleZoneInitPacket = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 45"); this.SetCurrentInstance = sig.ScanText("E8 ?? ?? ?? ?? 0F B6 55 ?? 48 8D 0D ?? ?? ?? ?? C0 EA"); // NetworkModuleProxy.SetCurrentInstance // These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used. From 3ef6135f15d84125906858914fffa51db23753bc Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Dec 2025 21:26:10 +0100 Subject: [PATCH 358/477] Fix reading ActiveFestivals in ZoneInitEventArgs --- Dalamud/Game/ClientState/ZoneInit.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/ClientState/ZoneInit.cs b/Dalamud/Game/ClientState/ZoneInit.cs index 5c2213c90..7eb4576aa 100644 --- a/Dalamud/Game/ClientState/ZoneInit.cs +++ b/Dalamud/Game/ClientState/ZoneInit.cs @@ -59,7 +59,7 @@ public class ZoneInitEventArgs : EventArgs eventArgs.ContentFinderCondition = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x06)); eventArgs.Weather = dataManager.GetExcelSheet().GetRow(*(byte*)(packet + 0x10)); - const int NumFestivals = 4; + const int NumFestivals = 8; eventArgs.ActiveFestivals = new Festival[NumFestivals]; eventArgs.ActiveFestivalPhases = new ushort[NumFestivals]; @@ -67,7 +67,7 @@ public class ZoneInitEventArgs : EventArgs // but it's unclear why they exist as separate entries and why they would be different. for (var i = 0; i < NumFestivals; i++) { - eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x2E + (i * 2))); + eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x26 + (i * 2))); eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2)); } From 1f5f6f89148e309e8d570cee5149d49f98ca0a91 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 20:45:31 +0000 Subject: [PATCH 359/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 2f0f4d2c8..63c259673 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 2f0f4d2c86989a7ef04bdbf975d0569948582fc9 +Subproject commit 63c2596738d2807d1d5ddf13c4dddbe0840d4df4 From da2b80156a957f704c2390f9ab174e9a3e56c7dd Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 20 Dec 2025 21:45:47 +0100 Subject: [PATCH 360/477] Fix some wording on badge tab --- .../Internal/Windows/Settings/Tabs/SettingsTabBadge.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs index 8e44ef7ea..e39c1952c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs @@ -20,7 +20,7 @@ internal sealed class SettingsTabBadge : SettingsTab public override string Title => Loc.Localize("DalamudSettingsBadge", "Badges"); - public override SettingsOpenKind Kind => SettingsOpenKind.ServerInfoBar; + public override SettingsOpenKind Kind => SettingsOpenKind.Badge; public override SettingsEntry[] Entries { get; } = [ @@ -36,12 +36,12 @@ internal sealed class SettingsTabBadge : SettingsTab var badgeManager = Service.Get(); var dalamudInterface = Service.Get(); - ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarHint", "Plugins can put additional information into your server information bar(where world & time can be seen).\nYou can reorder and disable these here.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingBadgesHint", "On this tab, you can unlock small badges that show on your title screen.\nBadge codes are usually given out during community events or contests.")); ImGuiHelpers.ScaledDummy(5); ImGui.Text(Loc.Localize("DalamudSettingsBadgesUnlock", "Unlock a badge")); - ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsBadgesUnlockHint", "If you have received a code for a badge, enter it here to unlock the badge.\nCodes are usually given out during community events or contests.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsBadgesUnlockHint", "If you have received a code for a badge, enter it here to unlock the badge.")); ImGui.InputTextWithHint( "##BadgePassword", Loc.Localize("DalamudSettingsBadgesUnlockHintInput", "Enter badge code here"), @@ -79,7 +79,7 @@ internal sealed class SettingsTabBadge : SettingsTab if (haveBadges.Length == 0) { - ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarDidNone", "You did not unlock any badges yet.\nBadges can be unlocked by participating in community events or contests.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsBadgesDidNone", "You did not unlock any badges yet.")); } var badgeTexture = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); From 3abddbae2ce516301b512f2f716b76deb3a37a95 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 21 Dec 2025 00:51:50 +0100 Subject: [PATCH 361/477] build: 14.0.0.1 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a2538ebd4..fc46cffa3 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 14.0.0.0 + 14.0.0.1 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 69c24fdbb9deceafe4b37a61680a5bada84f15af Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 01:23:12 +0000 Subject: [PATCH 362/477] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index d6ff8cf46..7737a7699 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit d6ff8cf46c7e341989843c28c7550f8d50bee851 +Subproject commit 7737a76995d6992e62fdc5d6a84871af4fe189bd From 96b5ad1b65fe4d4d3e0daecbe99d455c8a27e9b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 01:23:19 +0000 Subject: [PATCH 363/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 63c259673..7c3f1b81f 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 63c2596738d2807d1d5ddf13c4dddbe0840d4df4 +Subproject commit 7c3f1b81f5d3f1515bc8b3a62892d219638cee33 From a59efbd84c67c1560b117973df01cf31c54618ab Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 21 Dec 2025 02:41:22 +0100 Subject: [PATCH 364/477] Fixes for excel renamings --- Dalamud/Game/UnlockState/UnlockState.cs | 16 ++++++++-------- .../Windows/Data/Widgets/UIColorWidget.cs | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index cc70a524c..1878f54db 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -267,26 +267,26 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState switch ((ItemActionAction)row.ItemAction.Value.Action.RowId) { case ItemActionAction.Companion: - return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]); + return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.BuddyEquip: - return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]); + return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.Mount: - return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]); + return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.SecretRecipeBook: - return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]); + return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.UnlockLink: case ItemActionAction.OccultRecords: - return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]); + return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.TripleTriadCard when row.AdditionalData.Is(): return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId); case ItemActionAction.FolkloreTome: - return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]); + return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.OrchestrionRoll when row.AdditionalData.Is(): return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId); @@ -295,13 +295,13 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId); case ItemActionAction.Ornament: - return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]); + return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.Glasses: return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId); case ItemActionAction.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: - var supportJobId = (byte)row.ItemAction.Value.Data[0]; + var supportJobId = (byte)row.ItemAction.Value.Data[0].RowId; return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0; case ItemActionAction.CompanySealVouchers: diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index 3550f053c..fd3f1d11c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -126,17 +126,17 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_white"); - if (this.DrawColorColumn(row.Unknown0) && + if (this.DrawColorColumn(row.ClearWhite) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0); + DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite); ImGui.PopID(); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_green"); - if (this.DrawColorColumn(row.Unknown1) && + if (this.DrawColorColumn(row.ClearGreen) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1); + DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen); ImGui.PopID(); } } From f307aded73e9e4e4f0ea7fc19e13b324257dc395 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 20 Dec 2025 18:31:17 -0800 Subject: [PATCH 365/477] Lumina revert (#2544) --- Dalamud/Game/UnlockState/UnlockState.cs | 16 ++++++++-------- .../Windows/Data/Widgets/UIColorWidget.cs | 8 ++++---- lib/Lumina.Excel | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 1878f54db..cc70a524c 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -267,26 +267,26 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState switch ((ItemActionAction)row.ItemAction.Value.Action.RowId) { case ItemActionAction.Companion: - return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0].RowId); + return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.BuddyEquip: - return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0].RowId); + return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.Mount: - return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0].RowId); + return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.SecretRecipeBook: - return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0].RowId); + return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.UnlockLink: case ItemActionAction.OccultRecords: - return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0].RowId); + return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.TripleTriadCard when row.AdditionalData.Is(): return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId); case ItemActionAction.FolkloreTome: - return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0].RowId); + return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.OrchestrionRoll when row.AdditionalData.Is(): return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId); @@ -295,13 +295,13 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId); case ItemActionAction.Ornament: - return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0].RowId); + return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.Glasses: return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId); case ItemActionAction.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: - var supportJobId = (byte)row.ItemAction.Value.Data[0].RowId; + var supportJobId = (byte)row.ItemAction.Value.Data[0]; return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0; case ItemActionAction.CompanySealVouchers: diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index fd3f1d11c..3550f053c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -126,17 +126,17 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_white"); - if (this.DrawColorColumn(row.ClearWhite) && + if (this.DrawColorColumn(row.Unknown0) && adjacentRow.HasValue) - DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite); + DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0); ImGui.PopID(); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_green"); - if (this.DrawColorColumn(row.ClearGreen) && + if (this.DrawColorColumn(row.Unknown1) && adjacentRow.HasValue) - DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen); + DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1); ImGui.PopID(); } } diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 7737a7699..d6ff8cf46 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 7737a76995d6992e62fdc5d6a84871af4fe189bd +Subproject commit d6ff8cf46c7e341989843c28c7550f8d50bee851 From 5513bd1633f376213cf44691c2c74816931a57b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 11:59:55 +0000 Subject: [PATCH 366/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 7c3f1b81f..9c5f93cf3 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7c3f1b81f5d3f1515bc8b3a62892d219638cee33 +Subproject commit 9c5f93cf3ac57236656cd2323b93cd258ea84a88 From 4dcfa9da98173fbf81ad98300a44e6ac2a5df331 Mon Sep 17 00:00:00 2001 From: Infi Date: Sun, 21 Dec 2025 14:37:03 +0100 Subject: [PATCH 367/477] - Add ToDo for ulong change --- Dalamud/Game/ClientState/Party/PartyMember.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Game/ClientState/Party/PartyMember.cs b/Dalamud/Game/ClientState/Party/PartyMember.cs index c9980d9f2..84e3f21c8 100644 --- a/Dalamud/Game/ClientState/Party/PartyMember.cs +++ b/Dalamud/Game/ClientState/Party/PartyMember.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Utility; using Lumina.Excel; @@ -124,6 +125,7 @@ internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember public Vector3 Position => ptr->Position; /// + [Api15ToDo("Change type to ulong.")] public long ContentId => (long)ptr->ContentId; /// From e2a18dee5eea748d5a088f3937af1b30740fce88 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 21 Dec 2025 15:32:24 +0100 Subject: [PATCH 368/477] build: 14.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 fc46cffa3..5f79eb274 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 14.0.0.1 + 14.0.0.2 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 8ccfac231830104492d5b53d71c08d36e693f16c Mon Sep 17 00:00:00 2001 From: Soreepeong <3614868+Soreepeong@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:46:29 +0900 Subject: [PATCH 369/477] Fix wrong CancellationToken usage --- Dalamud/Interface/Internal/StaThreadService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/StaThreadService.cs b/Dalamud/Interface/Internal/StaThreadService.cs index bb5caa281..5e93bbf75 100644 --- a/Dalamud/Interface/Internal/StaThreadService.cs +++ b/Dalamud/Interface/Internal/StaThreadService.cs @@ -113,7 +113,7 @@ internal partial class StaThreadService : IInternalDisposableService using var cts = CancellationTokenSource.CreateLinkedTokenSource( this.cancellationTokenSource.Token, cancellationToken); - await this.taskFactory.StartNew(action, cancellationToken).ConfigureAwait(true); + await this.taskFactory.StartNew(action, cts.Token).ConfigureAwait(true); } /// Runs a given delegate in the messaging thread. @@ -126,7 +126,7 @@ internal partial class StaThreadService : IInternalDisposableService using var cts = CancellationTokenSource.CreateLinkedTokenSource( this.cancellationTokenSource.Token, cancellationToken); - return await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + return await this.taskFactory.StartNew(func, cts.Token).ConfigureAwait(true); } /// Runs a given delegate in the messaging thread. @@ -138,7 +138,7 @@ internal partial class StaThreadService : IInternalDisposableService using var cts = CancellationTokenSource.CreateLinkedTokenSource( this.cancellationTokenSource.Token, cancellationToken); - await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + await await this.taskFactory.StartNew(func, cts.Token).ConfigureAwait(true); } /// Runs a given delegate in the messaging thread. @@ -151,7 +151,7 @@ internal partial class StaThreadService : IInternalDisposableService using var cts = CancellationTokenSource.CreateLinkedTokenSource( this.cancellationTokenSource.Token, cancellationToken); - return await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + return await await this.taskFactory.StartNew(func, cts.Token).ConfigureAwait(true); } [LibraryImport("ole32.dll")] From 282fa87571c10b589c7a0b438db5cadb6d9d8533 Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:44:00 +0100 Subject: [PATCH 370/477] Add event for LogMessages being added to the chat --- Dalamud/Game/Chat/LogMessage.cs | 220 ++++++++++++++++++++++++++ Dalamud/Game/Chat/LogMessageEntity.cs | 96 +++++++++++ Dalamud/Game/Gui/ChatGui.cs | 58 +++++++ Dalamud/Plugin/Services/IChatGui.cs | 13 ++ 4 files changed, 387 insertions(+) create mode 100644 Dalamud/Game/Chat/LogMessage.cs create mode 100644 Dalamud/Game/Chat/LogMessageEntity.cs diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs new file mode 100644 index 000000000..cf423bd6e --- /dev/null +++ b/Dalamud/Game/Chat/LogMessage.cs @@ -0,0 +1,220 @@ +using Dalamud.Data; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Utility; + +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Component.Text; +using FFXIVClientStructs.Interop; + +using Lumina.Excel; + +using System.Diagnostics.CodeAnalysis; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Game.Chat; + +/// +/// Interface representing a log message. +/// +public interface ILogMessage : IEquatable +{ + /// + /// Gets the address of the log message in memory. + /// + nint Address { get; } + + /// + /// Gets the ID of this log message. + /// + uint LogMessageId { get; } + + /// + /// Gets the GameData associated with this log message. + /// + RowRef GameData { get; } + + /// + /// Gets the entity that is the source of this log message, if any. + /// + ILogMessageEntity? SourceEntity { get; } + + /// + /// Gets the entity that is the target of this log message, if any. + /// + ILogMessageEntity? TargetEntity { get; } + + /// + /// Gets the number of parameters. + /// + int ParameterCount { get; } + + /// + /// Retrieves the value of a parameter for the log message if it is an int. + /// + /// The index of the parameter to retrieve. + /// The value of the parameter. + /// if the parameter was retrieved successfully. + bool TryGetIntParameter(int index, out int value); + + /// + /// Retrieves the value of a parameter for the log message if it is a string. + /// + /// The index of the parameter to retrieve. + /// The value of the parameter. + /// if the parameter was retrieved successfully. + bool TryGetStringParameter(int index, [NotNullWhen(true)] out SeString? value); + + /// + /// Formats this log message into an approximation of the string that will eventually be shown in the log. + /// + /// This can cause side effects such as playing sound effects and thus should only be used for debugging. + /// The formatted string. + SeString FormatLogMessageForDebugging(); +} + +/// +/// This struct represents a status effect an actor is afflicted by. +/// +/// A pointer to the Status. +internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessage +{ + /// + public nint Address => (nint)ptr; + + /// + public uint LogMessageId => ptr->LogMessageId; + + /// + public RowRef GameData => LuminaUtils.CreateRef(ptr->LogMessageId); + + public LogMessageEntity SourceEntity => new LogMessageEntity(ptr, true); + /// + ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity; + + public LogMessageEntity TargetEntity => new LogMessageEntity(ptr, false); + + /// + ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity; + + /// + public int ParameterCount => ptr->Parameters.Count; + + public bool TryGetParameter(int index, out TextParameter value) + { + if (index < 0 || index >= ptr->Parameters.Count) + { + value = default; + return false; + } + + value = ptr->Parameters[index]; + return true; + } + + /// + public bool TryGetIntParameter(int index, out int value) + { + value = 0; + if (!this.TryGetParameter(index, out var parameter)) return false; + if (parameter.Type != TextParameterType.Integer) return false; + value = parameter.IntValue; + return true; + } + + /// + public bool TryGetStringParameter(int index, [NotNullWhen(true)] out SeString? value) + { + value = null; + if (!this.TryGetParameter(index, out var parameter)) return false; + if (parameter.Type == TextParameterType.String) + { + value = SeString.Parse(parameter.StringValue.Value); + return true; + } + if (parameter.Type == TextParameterType.ReferencedUtf8String) + { + value = SeString.Parse(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan()); + return true; + } + + return false; + } + + /// + public SeString FormatLogMessageForDebugging() + { + var logModule = RaptureLogModule.Instance(); + + // the formatting logic is taken from RaptureLogModule_Update + + var utf8 = new Utf8String(); + SetName(logModule, this.SourceEntity); + SetName(logModule, this.TargetEntity); + logModule->RaptureTextModule->FormatString(this.GameData.Value.Text.ToDalamudString().EncodeWithNullTerminator(), &ptr->Parameters, &utf8); + + return SeString.Parse(utf8.AsSpan()); + + void SetName(RaptureLogModule* self, LogMessageEntity item) + { + var name = item.NameSpan.GetPointer(0); + + if (item.IsPlayer) + { + var str = self->TempParseMessage.GetPointer(item.IsSourceEntity ? 8 : 9); + self->FormatPlayerLink(name, str, null, 0, item.Kind != 1 /* LocalPlayer */, item.HomeWorldId, false, null, false); + + if (item.HomeWorldId != 0 && item.HomeWorldId != AgentLobby.Instance()->LobbyData.HomeWorldId) + { + var crossWorldSymbol = self->RaptureTextModule->UnkStrings0.GetPointer(3); + if (!crossWorldSymbol->StringPtr.HasValue) + self->RaptureTextModule->ProcessMacroCode(crossWorldSymbol, "\0"u8); + str->Append(crossWorldSymbol); + if (self->UIModule->GetWorldHelper()->AllWorlds.TryGetValuePointer(item.HomeWorldId, out var world)) + str->ConcatCStr(world->Name); + } + + name = str->StringPtr; + } + + if (item.IsSourceEntity) + { + self->RaptureTextModule->SetGlobalTempEntity1(name, item.Sex, item.ObjStrId); + } + else + { + self->RaptureTextModule->SetGlobalTempEntity2(name, item.Sex, item.ObjStrId); + } + } + } + + + public static bool operator ==(LogMessage x, LogMessage y) => x.Equals(y); + + public static bool operator !=(LogMessage x, LogMessage y) => !(x == y); + + public bool Equals(LogMessage other) + { + return this.LogMessageId == other.LogMessageId && this.SourceEntity == other.SourceEntity && this.TargetEntity == other.TargetEntity; + } + + /// + public bool Equals(ILogMessage? other) + { + return other is LogMessage logMessage && this.Equals(logMessage); + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is LogMessage logMessage && this.Equals(logMessage); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.LogMessageId, this.SourceEntity, this.TargetEntity); + } +} diff --git a/Dalamud/Game/Chat/LogMessageEntity.cs b/Dalamud/Game/Chat/LogMessageEntity.cs new file mode 100644 index 000000000..e4c81c16a --- /dev/null +++ b/Dalamud/Game/Chat/LogMessageEntity.cs @@ -0,0 +1,96 @@ +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Data; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Client.UI.Misc; + +using Lumina.Excel; +using Lumina.Excel.Sheets; + +namespace Dalamud.Game.Chat; + +/// +/// Interface representing an entity related to a log message. +/// +public interface ILogMessageEntity : IEquatable +{ + /// + /// Gets the name of this entity. + /// + SeString Name { get; } + + /// + /// Gets the ID of the homeworld of this entity, if it is a player. + /// + ushort HomeWorldId { get; } + + /// + /// Gets the homeworld of this entity, if it is a player. + /// + RowRef HomeWorld { get; } + + /// + /// Gets the ObjStr ID of this entity, if not a player. See . + /// + uint ObjStrId { get; } + + /// + /// Gets a boolean indicating if this entity is a player. + /// + bool IsPlayer { get; } +} + + +/// +/// This struct represents a status effect an actor is afflicted by. +/// +/// A pointer to the Status. +internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity +{ + public Span NameSpan => source ? ptr->SourceName : ptr->TargetName; + + public SeString Name => SeString.Parse(this.NameSpan); + + public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld; + + public RowRef HomeWorld => LuminaUtils.CreateRef(this.HomeWorldId); + + public uint ObjStrId => source ? ptr->SourceObjStrId : ptr->TargetObjStrId; + + public byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind; + + public byte Sex => source ? ptr->SourceSex : ptr->TargetSex; + + public bool IsPlayer => source ? ptr->SourceIsPlayer : ptr->TargetIsPlayer; + + public bool IsSourceEntity => source; + + public static bool operator ==(LogMessageEntity x, LogMessageEntity y) => x.Equals(y); + + public static bool operator !=(LogMessageEntity x, LogMessageEntity y) => !(x == y); + + public bool Equals(LogMessageEntity other) + { + return this.Name == other.Name && this.HomeWorldId == other.HomeWorldId && this.ObjStrId == other.ObjStrId && this.Kind == other.Kind && this.Sex == other.Sex && this.IsPlayer == other.IsPlayer; + } + + /// + public bool Equals(ILogMessageEntity other) + { + return other is LogMessageEntity entity && this.Equals(entity); + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is LogMessageEntity entity && this.Equals(entity); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.Name, this.HomeWorldId, this.ObjStrId, this.Sex, this.IsPlayer); + } +} diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 30e2b676c..5208c019b 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; @@ -41,10 +42,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui private readonly Queue chatQueue = new(); private readonly Dictionary<(string PluginName, uint CommandId), Action> dalamudLinkHandlers = new(); + private readonly List seenLogMessageObjects = new(); private readonly Hook printMessageHook; private readonly Hook inventoryItemCopyHook; private readonly Hook handleLinkClickHook; + private readonly Hook handleLogModuleUpdate; [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); @@ -58,10 +61,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui this.printMessageHook = Hook.FromAddress(RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour); this.inventoryItemCopyHook = Hook.FromAddress((nint)InventoryItem.StaticVirtualTablePointer->Copy, this.InventoryItemCopyDetour); this.handleLinkClickHook = Hook.FromAddress(LogViewer.Addresses.HandleLinkClick.Value, this.HandleLinkClickDetour); + this.handleLogModuleUpdate = Hook.FromAddress(RaptureLogModule.Addresses.Update.Value, this.UpdateDetour); this.printMessageHook.Enable(); this.inventoryItemCopyHook.Enable(); this.handleLinkClickHook.Enable(); + this.handleLogModuleUpdate.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -79,6 +84,9 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui /// public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; + /// + public event IChatGui.OnLogMessageDelegate? LogMessage; + /// public uint LastLinkedItemId { get; private set; } @@ -110,6 +118,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui this.printMessageHook.Dispose(); this.inventoryItemCopyHook.Dispose(); this.handleLinkClickHook.Dispose(); + this.handleLogModuleUpdate.Dispose(); } #region DalamudSeString @@ -493,6 +502,46 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui Log.Error(ex, "Exception in HandleLinkClickDetour"); } } + + private void UpdateDetour(RaptureLogModule* thisPtr) + { + try + { + foreach (ref var item in thisPtr->LogMessageQueue) + { + var logMessage = new Chat.LogMessage((LogMessageQueueItem*)Unsafe.AsPointer(ref item)); + + // skip any entries that survived the previous Update call as the event was already called for them + if (this.seenLogMessageObjects.Contains(logMessage.Address)) + continue; + + foreach (var action in Delegate.EnumerateInvocationList(this.LogMessage)) + { + try + { + action(logMessage); + } + catch (Exception e) + { + Log.Error(e, "Could not invoke registered OnLogMessageDelegate for {Name}", action.Method); + } + } + } + + this.handleLogModuleUpdate.Original(thisPtr); + + // record the log messages for that we already called the event, but are still in the queue + this.seenLogMessageObjects.Clear(); + foreach (ref var item in thisPtr->LogMessageQueue) + { + this.seenLogMessageObjects.Add((IntPtr)Unsafe.AsPointer(ref item)); + } + } + catch (Exception ex) + { + Log.Error(ex, "Exception in UpdateDetour"); + } + } } /// @@ -521,6 +570,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui this.chatGuiService.CheckMessageHandled += this.OnCheckMessageForward; this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward; this.chatGuiService.ChatMessageUnhandled += this.OnMessageUnhandledForward; + this.chatGuiService.LogMessage += this.OnLogMessageForward; } /// @@ -535,6 +585,9 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui /// public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; + /// + public event IChatGui.OnLogMessageDelegate? LogMessage; + /// public uint LastLinkedItemId => this.chatGuiService.LastLinkedItemId; @@ -551,11 +604,13 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward; this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward; this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward; + this.chatGuiService.LogMessage -= this.OnLogMessageForward; this.ChatMessage = null; this.CheckMessageHandled = null; this.ChatMessageHandled = null; this.ChatMessageUnhandled = null; + this.LogMessage = null; } /// @@ -609,4 +664,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui private void OnMessageUnhandledForward(XivChatType type, int timestamp, SeString sender, SeString message) => this.ChatMessageUnhandled?.Invoke(type, timestamp, sender, message); + + private void OnLogMessageForward(Chat.ILogMessage message) + => this.LogMessage?.Invoke(message); } diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs index 572ac6c95..eec25cb5a 100644 --- a/Dalamud/Plugin/Services/IChatGui.cs +++ b/Dalamud/Plugin/Services/IChatGui.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; +using Dalamud.Game.Chat; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; @@ -50,6 +51,13 @@ public interface IChatGui : IDalamudService /// The message sent. public delegate void OnMessageUnhandledDelegate(XivChatType type, int timestamp, SeString sender, SeString message); + + /// + /// A delegate type used with the event. + /// + /// The message sent. + public delegate void OnLogMessageDelegate(ILogMessage message); + /// /// Event that will be fired when a chat message is sent to chat by the game. /// @@ -70,6 +78,11 @@ public interface IChatGui : IDalamudService /// public event OnMessageUnhandledDelegate ChatMessageUnhandled; + /// + /// Event that will be fired when a log message, that is a chat message based on entries in the LogMessage sheet, is sent. + /// + public event OnLogMessageDelegate LogMessage; + /// /// Gets the ID of the last linked item. /// From b2397efb25075b0b939f5eb5def9cde86b6e971a Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:21:07 +0100 Subject: [PATCH 371/477] Add Self Test --- .../SelfTest/Steps/ChatSelfTestStep.cs | 78 +++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs index 7a2631fbf..16fd3b01e 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs @@ -1,4 +1,5 @@ using Dalamud.Bindings.ImGui; +using Dalamud.Game.Chat; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; @@ -12,8 +13,12 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; internal class ChatSelfTestStep : ISelfTestStep { private int step = 0; - private bool subscribed = false; + private bool subscribedChatMessage = false; + private bool subscribedLogMessage = false; private bool hasPassed = false; + private bool hasTeleportGil = false; + private bool hasTeleportTicket = false; + private int teleportCount = 0; /// public string Name => "Test Chat"; @@ -34,20 +39,63 @@ internal class ChatSelfTestStep : ISelfTestStep case 1: ImGui.Text("Type \"/e DALAMUD\" in chat..."); - if (!this.subscribed) + if (!this.subscribedChatMessage) { - this.subscribed = true; + this.subscribedChatMessage = true; chatGui.ChatMessage += this.ChatOnOnChatMessage; } if (this.hasPassed) { chatGui.ChatMessage -= this.ChatOnOnChatMessage; - this.subscribed = false; - return SelfTestStepResult.Pass; + this.subscribedChatMessage = false; + this.step++; } break; + + case 2: + ImGui.Text("Teleport somewhere..."); + + if (!this.subscribedLogMessage) + { + this.subscribedLogMessage = true; + chatGui.LogMessage += this.ChatOnLogMessage; + } + + if (this.hasTeleportGil) + { + ImGui.Text($"You spent {this.teleportCount} gil to teleport."); + } + if (this.hasTeleportTicket) + { + ImGui.Text($"You used a ticket to teleport and have {this.teleportCount} remaining."); + } + + if (this.hasTeleportGil || this.hasTeleportTicket) + { + ImGui.Text("Is this correct?"); + + if (ImGui.Button("Yes")) + { + chatGui.LogMessage -= this.ChatOnLogMessage; + this.subscribedLogMessage = false; + this.step++; + } + + ImGui.SameLine(); + if (ImGui.Button("No")) + { + chatGui.LogMessage -= this.ChatOnLogMessage; + this.subscribedLogMessage = false; + return SelfTestStepResult.Fail; + } + } + + break; + + default: + return SelfTestStepResult.Pass; } return SelfTestStepResult.Waiting; @@ -59,7 +107,9 @@ internal class ChatSelfTestStep : ISelfTestStep var chatGui = Service.Get(); chatGui.ChatMessage -= this.ChatOnOnChatMessage; - this.subscribed = false; + chatGui.LogMessage -= this.ChatOnLogMessage; + this.subscribedChatMessage = false; + this.subscribedLogMessage = false; } private void ChatOnOnChatMessage( @@ -70,4 +120,20 @@ internal class ChatSelfTestStep : ISelfTestStep this.hasPassed = true; } } + + private void ChatOnLogMessage(ILogMessage message) + { + if (message.LogMessageId == 4590 && message.TryGetIntParameter(0, out var value)) + { + this.hasTeleportGil = true; + this.hasTeleportTicket = false; + this.teleportCount = value; + } + if (message.LogMessageId == 4591 && message.TryGetIntParameter(0, out var item) && item == 7569 && message.TryGetIntParameter(1, out var remaining)) + { + this.hasTeleportGil = false; + this.hasTeleportTicket = true; + this.teleportCount = remaining; + } + } } From 3aca09d0fb7fa94c769a898b68d96f7d6e630983 Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:28:44 +0100 Subject: [PATCH 372/477] review --- Dalamud/Game/Chat/LogMessage.cs | 10 ++++++---- Dalamud/Game/Gui/ChatGui.cs | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs index cf423bd6e..92217e1c6 100644 --- a/Dalamud/Game/Chat/LogMessage.cs +++ b/Dalamud/Game/Chat/LogMessage.cs @@ -12,7 +12,7 @@ using Lumina.Excel; using System.Diagnostics.CodeAnalysis; -using TerraFX.Interop.Windows; +using Lumina.Text.ReadOnly; namespace Dalamud.Game.Chat; @@ -72,7 +72,7 @@ public interface ILogMessage : IEquatable /// /// This can cause side effects such as playing sound effects and thus should only be used for debugging. /// The formatted string. - SeString FormatLogMessageForDebugging(); + ReadOnlySeString FormatLogMessageForDebugging(); } /// @@ -144,7 +144,7 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa } /// - public SeString FormatLogMessageForDebugging() + public ReadOnlySeString FormatLogMessageForDebugging() { var logModule = RaptureLogModule.Instance(); @@ -155,7 +155,9 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa SetName(logModule, this.TargetEntity); logModule->RaptureTextModule->FormatString(this.GameData.Value.Text.ToDalamudString().EncodeWithNullTerminator(), &ptr->Parameters, &utf8); - return SeString.Parse(utf8.AsSpan()); + var result = new ReadOnlySeString(utf8.AsSpan()); + utf8.Dtor(); + return result; void SetName(RaptureLogModule* self, LogMessageEntity item) { diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 5208c019b..c6405fb35 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -42,7 +42,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui private readonly Queue chatQueue = new(); private readonly Dictionary<(string PluginName, uint CommandId), Action> dalamudLinkHandlers = new(); - private readonly List seenLogMessageObjects = new(); + private readonly List seenLogMessageObjects = new(); private readonly Hook printMessageHook; private readonly Hook inventoryItemCopyHook; @@ -534,7 +534,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui this.seenLogMessageObjects.Clear(); foreach (ref var item in thisPtr->LogMessageQueue) { - this.seenLogMessageObjects.Add((IntPtr)Unsafe.AsPointer(ref item)); + this.seenLogMessageObjects.Add((nint)Unsafe.AsPointer(ref item)); } } catch (Exception ex) From 9da178ad569f6ff0809e1c6e023b54b962521b0e Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:34:04 +0100 Subject: [PATCH 373/477] review (2) --- Dalamud/Game/Chat/LogMessage.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs index 92217e1c6..fbe3dec3f 100644 --- a/Dalamud/Game/Chat/LogMessage.cs +++ b/Dalamud/Game/Chat/LogMessage.cs @@ -150,14 +150,12 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa // the formatting logic is taken from RaptureLogModule_Update - var utf8 = new Utf8String(); + using var utf8 = new Utf8String(); SetName(logModule, this.SourceEntity); SetName(logModule, this.TargetEntity); logModule->RaptureTextModule->FormatString(this.GameData.Value.Text.ToDalamudString().EncodeWithNullTerminator(), &ptr->Parameters, &utf8); - var result = new ReadOnlySeString(utf8.AsSpan()); - utf8.Dtor(); - return result; + return new ReadOnlySeString(utf8.AsSpan()); void SetName(RaptureLogModule* self, LogMessageEntity item) { From f76d77f79d09972d07cfd8aa030f2a560af17dc1 Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:41:46 +0100 Subject: [PATCH 374/477] rewview (3) and fix some copy docs comments --- Dalamud/Game/Chat/LogMessage.cs | 4 ++-- Dalamud/Game/Chat/LogMessageEntity.cs | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs index fbe3dec3f..931b3fb3a 100644 --- a/Dalamud/Game/Chat/LogMessage.cs +++ b/Dalamud/Game/Chat/LogMessage.cs @@ -76,9 +76,9 @@ public interface ILogMessage : IEquatable } /// -/// This struct represents a status effect an actor is afflicted by. +/// This struct represents log message in the queue to be added to the chat. /// -/// A pointer to the Status. +/// A pointer to the log message. internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessage { /// diff --git a/Dalamud/Game/Chat/LogMessageEntity.cs b/Dalamud/Game/Chat/LogMessageEntity.cs index e4c81c16a..b465dc158 100644 --- a/Dalamud/Game/Chat/LogMessageEntity.cs +++ b/Dalamud/Game/Chat/LogMessageEntity.cs @@ -1,13 +1,13 @@ using System.Diagnostics.CodeAnalysis; using Dalamud.Data; -using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Lumina.Excel; using Lumina.Excel.Sheets; +using Lumina.Text.ReadOnly; namespace Dalamud.Game.Chat; @@ -19,7 +19,7 @@ public interface ILogMessageEntity : IEquatable /// /// Gets the name of this entity. /// - SeString Name { get; } + ReadOnlySeString Name { get; } /// /// Gets the ID of the homeworld of this entity, if it is a player. @@ -44,14 +44,15 @@ public interface ILogMessageEntity : IEquatable /// -/// This struct represents a status effect an actor is afflicted by. +/// This struct represents an entity related to a log message. /// -/// A pointer to the Status. +/// A pointer to the log message item. +/// If represents the source entity of the log message, otherwise represents the target entity internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity { public Span NameSpan => source ? ptr->SourceName : ptr->TargetName; - public SeString Name => SeString.Parse(this.NameSpan); + public ReadOnlySeString Name => new ReadOnlySeString(this.NameSpan); public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld; From 186b1b83765cd17d58fd70dc6c53a26af70b85ff Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:58:47 +0100 Subject: [PATCH 375/477] Use SeStringEvaluator instead of RaptureTextModule for the debug display --- Dalamud/Game/Chat/LogMessage.cs | 13 +++++-------- Dalamud/Game/Text/Evaluator/SeStringParameter.cs | 10 ++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs index 931b3fb3a..37b341428 100644 --- a/Dalamud/Game/Chat/LogMessage.cs +++ b/Dalamud/Game/Chat/LogMessage.cs @@ -1,18 +1,18 @@ using Dalamud.Data; +using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Client.System.String; + using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.Text; using FFXIVClientStructs.Interop; using Lumina.Excel; +using Lumina.Text.ReadOnly; using System.Diagnostics.CodeAnalysis; - -using Lumina.Text.ReadOnly; +using System.Linq; namespace Dalamud.Game.Chat; @@ -150,12 +150,9 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa // the formatting logic is taken from RaptureLogModule_Update - using var utf8 = new Utf8String(); SetName(logModule, this.SourceEntity); SetName(logModule, this.TargetEntity); - logModule->RaptureTextModule->FormatString(this.GameData.Value.Text.ToDalamudString().EncodeWithNullTerminator(), &ptr->Parameters, &utf8); - - return new ReadOnlySeString(utf8.AsSpan()); + return Service.Get().EvaluateFromLogMessage(this.LogMessageId, ptr->Parameters.Select(p => (SeStringParameter)p).ToArray()); void SetName(RaptureLogModule* self, LogMessageEntity item) { diff --git a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs index 036d1c921..a8fe3b3b9 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs @@ -1,5 +1,7 @@ using System.Globalization; +using FFXIVClientStructs.FFXIV.Component.Text; + using Lumina.Text.ReadOnly; using DSeString = Dalamud.Game.Text.SeStringHandling.SeString; @@ -75,4 +77,12 @@ public readonly struct SeStringParameter public static implicit operator SeStringParameter(string value) => new(value); public static implicit operator SeStringParameter(ReadOnlySpan value) => new(value); + + public static unsafe implicit operator SeStringParameter(TextParameter value) => value.Type switch + { + TextParameterType.Uninitialized => default, + TextParameterType.Integer => new((uint)value.IntValue), + TextParameterType.ReferencedUtf8String => new(new ReadOnlySeString(value.ReferencedUtf8StringValue->Utf8String.AsSpan())), + TextParameterType.String => new(value.StringValue), + }; } From bf75937cc05d542504039d375efdf477d6f2e435 Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:01:29 +0100 Subject: [PATCH 376/477] Don't go through SeString to null terminate a string --- Dalamud/Game/Chat/LogMessage.cs | 12 ++++++++---- Dalamud/Game/Text/Evaluator/SeStringParameter.cs | 10 ---------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs index 37b341428..5d59a84ee 100644 --- a/Dalamud/Game/Chat/LogMessage.cs +++ b/Dalamud/Game/Chat/LogMessage.cs @@ -1,8 +1,8 @@ using Dalamud.Data; -using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Utility; - +using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.Text; @@ -12,7 +12,6 @@ using Lumina.Excel; using Lumina.Text.ReadOnly; using System.Diagnostics.CodeAnalysis; -using System.Linq; namespace Dalamud.Game.Chat; @@ -150,9 +149,14 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa // the formatting logic is taken from RaptureLogModule_Update + using var utf8 = new Utf8String(); SetName(logModule, this.SourceEntity); SetName(logModule, this.TargetEntity); - return Service.Get().EvaluateFromLogMessage(this.LogMessageId, ptr->Parameters.Select(p => (SeStringParameter)p).ToArray()); + + using var rssb = new RentedSeStringBuilder(); + logModule->RaptureTextModule->FormatString(rssb.Builder.Append(this.GameData.Value.Text).GetViewAsSpan(), &ptr->Parameters, &utf8); + + return new ReadOnlySeString(utf8.AsSpan()); void SetName(RaptureLogModule* self, LogMessageEntity item) { diff --git a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs index a8fe3b3b9..036d1c921 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs @@ -1,7 +1,5 @@ using System.Globalization; -using FFXIVClientStructs.FFXIV.Component.Text; - using Lumina.Text.ReadOnly; using DSeString = Dalamud.Game.Text.SeStringHandling.SeString; @@ -77,12 +75,4 @@ public readonly struct SeStringParameter public static implicit operator SeStringParameter(string value) => new(value); public static implicit operator SeStringParameter(ReadOnlySpan value) => new(value); - - public static unsafe implicit operator SeStringParameter(TextParameter value) => value.Type switch - { - TextParameterType.Uninitialized => default, - TextParameterType.Integer => new((uint)value.IntValue), - TextParameterType.ReferencedUtf8String => new(new ReadOnlySeString(value.ReferencedUtf8StringValue->Utf8String.AsSpan())), - TextParameterType.String => new(value.StringValue), - }; } From 65c604f8272b6083175cd42945b4983b2f5cce98 Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Wed, 24 Dec 2025 11:13:06 +0100 Subject: [PATCH 377/477] More ReadOnlySeString things --- Dalamud/Game/Chat/LogMessage.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs index 5d59a84ee..93b928d48 100644 --- a/Dalamud/Game/Chat/LogMessage.cs +++ b/Dalamud/Game/Chat/LogMessage.cs @@ -1,5 +1,4 @@ using Dalamud.Data; -using Dalamud.Game.Text.SeStringHandling; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.System.String; @@ -64,7 +63,7 @@ public interface ILogMessage : IEquatable /// The index of the parameter to retrieve. /// The value of the parameter. /// if the parameter was retrieved successfully. - bool TryGetStringParameter(int index, [NotNullWhen(true)] out SeString? value); + bool TryGetStringParameter(int index, out ReadOnlySeString value); /// /// Formats this log message into an approximation of the string that will eventually be shown in the log. @@ -124,18 +123,18 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa } /// - public bool TryGetStringParameter(int index, [NotNullWhen(true)] out SeString? value) + public bool TryGetStringParameter(int index, out ReadOnlySeString value) { - value = null; + value = default; if (!this.TryGetParameter(index, out var parameter)) return false; if (parameter.Type == TextParameterType.String) { - value = SeString.Parse(parameter.StringValue.Value); + value = new(parameter.StringValue.AsSpan()); return true; } if (parameter.Type == TextParameterType.ReferencedUtf8String) { - value = SeString.Parse(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan()); + value = new(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan()); return true; } From 31cbf4d8eb47782cd332f41e0af8b74d10ebfb8a Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:17:57 +0100 Subject: [PATCH 378/477] Respect null-termination of entity names --- Dalamud/Game/Chat/LogMessageEntity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Chat/LogMessageEntity.cs b/Dalamud/Game/Chat/LogMessageEntity.cs index b465dc158..4294e4898 100644 --- a/Dalamud/Game/Chat/LogMessageEntity.cs +++ b/Dalamud/Game/Chat/LogMessageEntity.cs @@ -52,7 +52,7 @@ internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool { public Span NameSpan => source ? ptr->SourceName : ptr->TargetName; - public ReadOnlySeString Name => new ReadOnlySeString(this.NameSpan); + public ReadOnlySeString Name => new ReadOnlySeString(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]); public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld; From c559426d8b6da76952b04a4cefc18fa6494d2c91 Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:23:24 +0100 Subject: [PATCH 379/477] Add Data Widget --- .../Internal/Windows/Data/DataWindow.cs | 1 + .../Data/Widgets/LogMessageMonitorWidget.cs | 156 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/LogMessageMonitorWidget.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index eb0589d59..154fc8c02 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -44,6 +44,7 @@ internal class DataWindow : Window, IDisposable new ImGuiWidget(), new InventoryWidget(), new KeyStateWidget(), + new LogMessageMonitorWidget(), new MarketBoardWidget(), new NetworkMonitorWidget(), new NounProcessorWidget(), diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/LogMessageMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/LogMessageMonitorWidget.cs new file mode 100644 index 000000000..fde46f0c7 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/LogMessageMonitorWidget.cs @@ -0,0 +1,156 @@ +using System.Buffers; +using System.Collections.Concurrent; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; + +using Dalamud.Bindings.ImGui; +using Dalamud.Game.Chat; +using Dalamud.Game.Gui; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; + +using Lumina.Text.ReadOnly; + +using ImGuiTable = Dalamud.Interface.Utility.ImGuiTable; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget to display the LogMessages. +/// +internal class LogMessageMonitorWidget : IDataWindowWidget +{ + private readonly ConcurrentQueue messages = new(); + + private bool trackMessages; + private int trackedMessages; + private Regex? filterRegex; + private string filterString = string.Empty; + + /// + public string[]? CommandShortcuts { get; init; } = ["logmessage"]; + + /// + public string DisplayName { get; init; } = "LogMessage Monitor"; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.trackMessages = false; + this.trackedMessages = 20; + this.filterRegex = null; + this.filterString = string.Empty; + this.messages.Clear(); + this.Ready = true; + } + + /// + public void Draw() + { + var network = Service.Get(); + if (ImGui.Checkbox("Track LogMessages"u8, ref this.trackMessages)) + { + if (this.trackMessages) + { + network.LogMessage += this.OnLogMessage; + } + else + { + network.LogMessage -= this.OnLogMessage; + } + } + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2); + if (ImGui.DragInt("Stored Number of Messages"u8, ref this.trackedMessages, 0.1f, 1, 512)) + { + this.trackedMessages = Math.Clamp(this.trackedMessages, 1, 512); + } + + if (ImGui.Button("Clear Stored Messages"u8)) + { + this.messages.Clear(); + } + + this.DrawFilterInput(); + + ImGuiTable.DrawTable(string.Empty, this.messages.Where(m => this.filterRegex == null || this.filterRegex.IsMatch(m.Formatted.ExtractText())), this.DrawNetworkPacket, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp, "LogMessageId", "Source", "Target", "Parameters", "Formatted"); + } + + private void DrawNetworkPacket(LogMessageData data) + { + ImGui.TableNextColumn(); + ImGui.Text(data.LogMessageId.ToString()); + + ImGui.TableNextColumn(); + ImGuiHelpers.SeStringWrapped(data.Source); + + ImGui.TableNextColumn(); + ImGuiHelpers.SeStringWrapped(data.Target); + + ImGui.TableNextColumn(); + ImGui.Text(data.Parameters); + + ImGui.TableNextColumn(); + ImGuiHelpers.SeStringWrapped(data.Formatted); + } + + private void DrawFilterInput() + { + var invalidRegEx = this.filterString.Length > 0 && this.filterRegex == null; + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx); + using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (!ImGui.InputTextWithHint("##Filter"u8, "Regex Filter..."u8, ref this.filterString, 1024)) + { + return; + } + + if (this.filterString.Length == 0) + { + this.filterRegex = null; + } + else + { + try + { + this.filterRegex = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture); + } + catch + { + this.filterRegex = null; + } + } + } + + private void OnLogMessage(ILogMessage message) + { + var buffer = new ArrayBufferWriter(); + var writer = new Utf8JsonWriter(buffer); + + writer.WriteStartArray(); + for (var i = 0; i < message.ParameterCount; i++) + { + if (message.TryGetStringParameter(i, out var str)) + writer.WriteStringValue(str.ExtractText()); + else if (message.TryGetIntParameter(i, out var num)) + writer.WriteNumberValue(num); + else + writer.WriteNullValue(); + } + + writer.WriteEndArray(); + writer.Flush(); + + this.messages.Enqueue(new LogMessageData(message.LogMessageId, message.SourceEntity?.Name ?? default, message.TargetEntity?.Name ?? default, buffer.WrittenMemory, message.FormatLogMessageForDebugging())); + while (this.messages.Count > this.trackedMessages) + { + this.messages.TryDequeue(out _); + } + } + + private readonly record struct LogMessageData(uint LogMessageId, ReadOnlySeString Source, ReadOnlySeString Target, ReadOnlyMemory Parameters, ReadOnlySeString Formatted); +} From c00363badfe4946fa6cb82df62b8369c67341d26 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 25 Dec 2025 11:31:47 +0100 Subject: [PATCH 380/477] build: 14.0.0.3 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 5f79eb274..f5e75af63 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 14.0.0.2 + 14.0.0.3 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 558a011e00cb85b3b306a524e38f51d76e342625 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 15:02:13 +0000 Subject: [PATCH 381/477] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index d6ff8cf46..c4ea8bafd 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit d6ff8cf46c7e341989843c28c7550f8d50bee851 +Subproject commit c4ea8bafda8d88d49a390014dd3b86457f2dc7d5 From 689d2f01d9cd642bb77a249c22eac6c8d9884d67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 15:02:13 +0000 Subject: [PATCH 382/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 9c5f93cf3..2c3b35dd7 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9c5f93cf3ac57236656cd2323b93cd258ea84a88 +Subproject commit 2c3b35dd7da71a94b517536affa180fd0e7dc403 From 62b8b0834cefa190e749a67a311ecc3e92dfc346 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 26 Dec 2025 16:09:32 +0100 Subject: [PATCH 383/477] Use v145 build tools for C++ components --- Dalamud.Boot/Dalamud.Boot.vcxproj | 4 ++-- DalamudCrashHandler/DalamudCrashHandler.vcxproj | 4 ++-- external/cimgui/cimgui.vcxproj | 6 +++--- external/cimguizmo/cimguizmo.vcxproj | 6 +++--- external/cimplot/cimplot.vcxproj | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index 0a4a9c563..20c107be2 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -25,7 +25,7 @@ DynamicLibrary true - v143 + v145 false Unicode bin\$(Configuration)\ @@ -211,4 +211,4 @@ - \ No newline at end of file + diff --git a/DalamudCrashHandler/DalamudCrashHandler.vcxproj b/DalamudCrashHandler/DalamudCrashHandler.vcxproj index 00d67cd65..e332b7b03 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.vcxproj +++ b/DalamudCrashHandler/DalamudCrashHandler.vcxproj @@ -25,7 +25,7 @@ Application true - v143 + v145 false Unicode ..\bin\$(Configuration)\ @@ -95,4 +95,4 @@ - \ No newline at end of file + diff --git a/external/cimgui/cimgui.vcxproj b/external/cimgui/cimgui.vcxproj index d99d23119..e047718ac 100644 --- a/external/cimgui/cimgui.vcxproj +++ b/external/cimgui/cimgui.vcxproj @@ -35,13 +35,13 @@ DynamicLibrary true - v143 + v145 Unicode DynamicLibrary false - v143 + v145 true Unicode @@ -106,4 +106,4 @@ - \ No newline at end of file + diff --git a/external/cimguizmo/cimguizmo.vcxproj b/external/cimguizmo/cimguizmo.vcxproj index 9bf692d4b..b3d66fb81 100644 --- a/external/cimguizmo/cimguizmo.vcxproj +++ b/external/cimguizmo/cimguizmo.vcxproj @@ -37,13 +37,13 @@ DynamicLibrary true - v143 + v145 Unicode DynamicLibrary false - v143 + v145 true Unicode @@ -108,4 +108,4 @@ - \ No newline at end of file + diff --git a/external/cimplot/cimplot.vcxproj b/external/cimplot/cimplot.vcxproj index e9aecbc15..cdc4226bf 100644 --- a/external/cimplot/cimplot.vcxproj +++ b/external/cimplot/cimplot.vcxproj @@ -35,13 +35,13 @@ DynamicLibrary true - v143 + v145 Unicode DynamicLibrary false - v143 + v145 true Unicode @@ -106,4 +106,4 @@ - \ No newline at end of file + From a659cd8a49df7c7370d7596e946202e9c5d1d3be Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 26 Dec 2025 16:15:51 +0100 Subject: [PATCH 384/477] Adjust to Excel renames --- .../Internal/Windows/Data/Widgets/UIColorWidget.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index 3550f053c..fd3f1d11c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -126,17 +126,17 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_white"); - if (this.DrawColorColumn(row.Unknown0) && + if (this.DrawColorColumn(row.ClearWhite) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0); + DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite); ImGui.PopID(); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_green"); - if (this.DrawColorColumn(row.Unknown1) && + if (this.DrawColorColumn(row.ClearGreen) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1); + DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen); ImGui.PopID(); } } From fc130e325cf967069e8c0b0b9026dd3833af42fc Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 26 Dec 2025 16:15:58 +0100 Subject: [PATCH 385/477] Fix warnings --- Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index 4d858922a..56ed45446 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -25,9 +25,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class UldWidget : IDataWindowWidget { + private const string UldBaseBath = "ui/uld/"; + // ULD styles can be hardcoded for now as they don't add new ones regularly. Can later try and find where to load these from in the game EXE. private static readonly string[] ThemeDisplayNames = ["Dark", "Light", "Classic FF", "Clear Blue", "Clear White", "Clear Green"]; - private const string UldBaseBath = "ui/uld/"; // 48 8D 15 ?? ?? ?? ?? is the part of the signatures that contain the string location offset // 48 = 64 bit register prefix @@ -263,7 +264,7 @@ internal class UldWidget : IDataWindowWidget } private string ToThemedPath(string path) => - UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : "") + path[UldBaseBath.Length..]; + UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : string.Empty) + path[UldBaseBath.Length..]; private void DrawTextureEntry(UldRoot.TextureEntry textureEntry, TextureManager textureManager) { From 79ce2fff0aba5eaa827d5c7696fb0b07f790e865 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 31 Dec 2025 12:15:18 +0100 Subject: [PATCH 386/477] Revert "Use v145 build tools for C++ components" This reverts commit 62b8b0834cefa190e749a67a311ecc3e92dfc346. MS can't manage to get actions images with 2026 for some reason. --- Dalamud.Boot/Dalamud.Boot.vcxproj | 4 ++-- DalamudCrashHandler/DalamudCrashHandler.vcxproj | 4 ++-- external/cimgui/cimgui.vcxproj | 6 +++--- external/cimguizmo/cimguizmo.vcxproj | 6 +++--- external/cimplot/cimplot.vcxproj | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index 20c107be2..0a4a9c563 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -25,7 +25,7 @@ DynamicLibrary true - v145 + v143 false Unicode bin\$(Configuration)\ @@ -211,4 +211,4 @@ - + \ No newline at end of file diff --git a/DalamudCrashHandler/DalamudCrashHandler.vcxproj b/DalamudCrashHandler/DalamudCrashHandler.vcxproj index e332b7b03..00d67cd65 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.vcxproj +++ b/DalamudCrashHandler/DalamudCrashHandler.vcxproj @@ -25,7 +25,7 @@ Application true - v145 + v143 false Unicode ..\bin\$(Configuration)\ @@ -95,4 +95,4 @@ - + \ No newline at end of file diff --git a/external/cimgui/cimgui.vcxproj b/external/cimgui/cimgui.vcxproj index e047718ac..d99d23119 100644 --- a/external/cimgui/cimgui.vcxproj +++ b/external/cimgui/cimgui.vcxproj @@ -35,13 +35,13 @@ DynamicLibrary true - v145 + v143 Unicode DynamicLibrary false - v145 + v143 true Unicode @@ -106,4 +106,4 @@ - + \ No newline at end of file diff --git a/external/cimguizmo/cimguizmo.vcxproj b/external/cimguizmo/cimguizmo.vcxproj index b3d66fb81..9bf692d4b 100644 --- a/external/cimguizmo/cimguizmo.vcxproj +++ b/external/cimguizmo/cimguizmo.vcxproj @@ -37,13 +37,13 @@ DynamicLibrary true - v145 + v143 Unicode DynamicLibrary false - v145 + v143 true Unicode @@ -108,4 +108,4 @@ - + \ No newline at end of file diff --git a/external/cimplot/cimplot.vcxproj b/external/cimplot/cimplot.vcxproj index cdc4226bf..e9aecbc15 100644 --- a/external/cimplot/cimplot.vcxproj +++ b/external/cimplot/cimplot.vcxproj @@ -35,13 +35,13 @@ DynamicLibrary true - v145 + v143 Unicode DynamicLibrary false - v145 + v143 true Unicode @@ -106,4 +106,4 @@ - + \ No newline at end of file From 49abb19673c035f2b0db685595089a7567abe817 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:49:51 +0900 Subject: [PATCH 387/477] Fix font corruption caused by Font Awesome icon button in Data window --- .../Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 4f5540daf..ea4b80247 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -114,7 +114,7 @@ internal class FontAwesomeTestWidget : IDataWindowWidget Task.FromResult( Service.Get().CreateTextureFromSeString( ReadOnlySeString.FromText(this.icons[i].ToIconString()), - new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize() }))); + new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize(), ScreenOffset = Vector2.Zero }))); } ImGui.PopFont(); From bbb6e438b1fd963584a9b4091d03370738a29934 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:31:20 +0100 Subject: [PATCH 388/477] Update ClientStructs (#2556) 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 2c3b35dd7..e1e99cf46 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 2c3b35dd7da71a94b517536affa180fd0e7dc403 +Subproject commit e1e99cf469f87b0f3e8664ee3e3650dff86df2d6 From 5a0257e40e37ce2b0d77563f2d46c03d4387914f Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:31:46 +0100 Subject: [PATCH 389/477] Update Excel Schema (#2555) Co-authored-by: github-actions[bot] --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index c4ea8bafd..52cb5e0a9 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit c4ea8bafda8d88d49a390014dd3b86457f2dc7d5 +Subproject commit 52cb5e0a9a7a1138d8c2406c277307a6c9ad8898 From 9538af05541fb037cf1e5db3126da5f2cc663b61 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 3 Jan 2026 20:46:48 +0100 Subject: [PATCH 390/477] UldWidget fixes (#2557) * Fix missing directory separator in theme path * Hide themed texture exception when file not found * Check ThemeSupportBitmask --- .../Windows/Data/Widgets/UldWidget.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index 56ed45446..7c8110301 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -47,6 +47,7 @@ internal class UldWidget : IDataWindowWidget ("48 8D 15 ?? ?? ?? ?? 45 33 C0 E9 ?? ?? ?? ??", 3) ]; + private DataManager dataManager; private CancellationTokenSource? cts; private Task? uldNamesTask; @@ -69,6 +70,8 @@ internal class UldWidget : IDataWindowWidget /// public void Load() { + this.dataManager ??= Service.Get(); + this.cts?.Cancel(); ClearTask(ref this.uldNamesTask); this.uldNamesTask = null; @@ -264,7 +267,7 @@ internal class UldWidget : IDataWindowWidget } private string ToThemedPath(string path) => - UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : string.Empty) + path[UldBaseBath.Length..]; + UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}/" : string.Empty) + path[UldBaseBath.Length..]; private void DrawTextureEntry(UldRoot.TextureEntry textureEntry, TextureManager textureManager) { @@ -292,14 +295,17 @@ internal class UldWidget : IDataWindowWidget else if (e is not null) ImGui.Text(e.ToString()); - if (this.selectedTheme != 0) + if (this.selectedTheme != 0 && (textureEntry.ThemeSupportBitmask & (1 << (this.selectedTheme - 1))) != 0) { var texturePathThemed = this.ToThemedPath(texturePath); - ImGui.Text($"Themed path at {texturePathThemed}:"); - if (textureManager.Shared.GetFromGame(texturePathThemed).TryGetWrap(out wrap, out e)) - ImGui.Image(wrap.Handle, wrap.Size); - else if (e is not null) - ImGui.Text(e.ToString()); + if (this.dataManager.FileExists(texturePathThemed)) + { + ImGui.Text($"Themed path at {texturePathThemed}:"); + if (textureManager.Shared.GetFromGame(texturePathThemed).TryGetWrap(out wrap, out e)) + ImGui.Image(wrap.Handle, wrap.Size); + else if (e is not null) + ImGui.Text(e.ToString()); + } } ImGui.EndTooltip(); From a1d2e275a78680fa1fc33a693f453189012ab4c5 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 3 Jan 2026 20:54:24 +0100 Subject: [PATCH 391/477] - Apply ImRaii to FontAwesomeTestWidget - Adjust array init --- .../Data/Widgets/FontAwesomeTestWidget.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index ea4b80247..8435f18dd 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -5,9 +5,11 @@ using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Internal; +using Dalamud.Interface.Utility.Raii; using Lumina.Text.ReadOnly; @@ -18,13 +20,15 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class FontAwesomeTestWidget : IDataWindowWidget { + private static readonly string[] First = ["(Show All)", "(Undefined)"]; + private List? icons; private List? iconNames; private string[]? iconCategories; private int selectedIconCategory; private string iconSearchInput = string.Empty; private bool iconSearchChanged = true; - private bool useFixedWidth = false; + private bool useFixedWidth; /// public string[]? CommandShortcuts { get; init; } = { "fa", "fatest", "fontawesome" }; @@ -44,11 +48,9 @@ internal class FontAwesomeTestWidget : IDataWindowWidget /// public void Draw() { - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + using var pushedStyle = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - this.iconCategories ??= new[] { "(Show All)", "(Undefined)" } - .Concat(FontAwesomeHelpers.GetCategories().Skip(1)) - .ToArray(); + this.iconCategories ??= First.Concat(FontAwesomeHelpers.GetCategories().Skip(1)).ToArray(); if (this.iconSearchChanged) { @@ -101,7 +103,8 @@ internal class FontAwesomeTestWidget : IDataWindowWidget ImGuiHelpers.ScaledRelativeSameLine(50f); ImGui.Text($"{this.iconNames?[i]}"); ImGuiHelpers.ScaledRelativeSameLine(280f); - ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont); + + using var pushedFont = ImRaii.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont); ImGui.Text(this.icons[i].ToIconString()); ImGuiHelpers.ScaledRelativeSameLine(320f); if (this.useFixedWidth @@ -114,13 +117,10 @@ internal class FontAwesomeTestWidget : IDataWindowWidget Task.FromResult( Service.Get().CreateTextureFromSeString( ReadOnlySeString.FromText(this.icons[i].ToIconString()), - new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize(), ScreenOffset = Vector2.Zero }))); + new SeStringDrawParams { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize(), ScreenOffset = Vector2.Zero }))); } - ImGui.PopFont(); ImGuiHelpers.ScaledDummy(2f); } - - ImGui.PopStyleVar(); } } From e44fda19115b1d1f94a5ddcbe90f7d0555ae5c16 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 3 Jan 2026 21:04:01 +0100 Subject: [PATCH 392/477] - Apply ImRaii to UIColorWidget --- .../Windows/Data/Widgets/UIColorWidget.cs | 73 ++++++++++--------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index fd3f1d11c..bc6e5376c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -7,6 +7,8 @@ using Dalamud.Data; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.ImGuiSeStringRenderer.Internal; +using Dalamud.Interface.Utility.Raii; + using Lumina.Excel.Sheets; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -32,7 +34,7 @@ internal class UiColorWidget : IDataWindowWidget } /// - public unsafe void Draw() + public void Draw() { var colors = Service.GetNullable()?.GetExcelSheet() ?? throw new InvalidOperationException("UIColor sheet not loaded."); @@ -44,7 +46,9 @@ internal class UiColorWidget : IDataWindowWidget "BB.
" + "· Click on a color to copy the color code.
" + "· Hover on a color to preview the text with edge, when the next color has been used together."); - if (!ImGui.BeginTable("UIColor"u8, 7)) + + using var table = ImRaii.Table("UIColor"u8, 7); + if (!table.Success) return; ImGui.TableSetupScrollFreeze(0, 1); @@ -93,61 +97,61 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_dark"); - if (this.DrawColorColumn(row.Dark) && - adjacentRow.HasValue) - DrawEdgePreview(id, row.Dark, adjacentRow.Value.Dark); - ImGui.PopID(); + using (ImRaii.PushId($"row{id}_dark")) + { + if (this.DrawColorColumn(row.Dark) && adjacentRow.HasValue) + DrawEdgePreview(id, row.Dark, adjacentRow.Value.Dark); + } ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_light"); - if (this.DrawColorColumn(row.Light) && - adjacentRow.HasValue) - DrawEdgePreview(id, row.Light, adjacentRow.Value.Light); - ImGui.PopID(); + using (ImRaii.PushId($"row{id}_light")) + { + if (this.DrawColorColumn(row.Light) && adjacentRow.HasValue) + DrawEdgePreview(id, row.Light, adjacentRow.Value.Light); + } ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_classic"); - if (this.DrawColorColumn(row.ClassicFF) && - adjacentRow.HasValue) - DrawEdgePreview(id, row.ClassicFF, adjacentRow.Value.ClassicFF); - ImGui.PopID(); + using (ImRaii.PushId($"row{id}_classic")) + { + if (this.DrawColorColumn(row.ClassicFF) && adjacentRow.HasValue) + DrawEdgePreview(id, row.ClassicFF, adjacentRow.Value.ClassicFF); + } ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_blue"); - if (this.DrawColorColumn(row.ClearBlue) && - adjacentRow.HasValue) - DrawEdgePreview(id, row.ClearBlue, adjacentRow.Value.ClearBlue); - ImGui.PopID(); + using (ImRaii.PushId($"row{id}_blue")) + { + if (this.DrawColorColumn(row.ClearBlue) && adjacentRow.HasValue) + DrawEdgePreview(id, row.ClearBlue, adjacentRow.Value.ClearBlue); + } ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_white"); - if (this.DrawColorColumn(row.ClearWhite) && - adjacentRow.HasValue) - DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite); - ImGui.PopID(); + using (ImRaii.PushId($"row{id}_white")) + { + if (this.DrawColorColumn(row.ClearWhite) && adjacentRow.HasValue) + DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite); + } ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.PushID($"row{id}_green"); - if (this.DrawColorColumn(row.ClearGreen) && - adjacentRow.HasValue) - DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen); - ImGui.PopID(); + using (ImRaii.PushId($"row{id}_green")) + { + if (this.DrawColorColumn(row.ClearGreen) && adjacentRow.HasValue) + DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen); + } } } clipper.Destroy(); - ImGui.EndTable(); } private static void DrawEdgePreview(uint id, uint sheetColor, uint sheetColor2) { - ImGui.BeginTooltip(); + using var tooltip = ImRaii.Tooltip(); + Span buf = stackalloc byte[256]; var ptr = 0; ptr += Encoding.UTF8.GetBytes(" Date: Sat, 3 Jan 2026 21:09:20 +0100 Subject: [PATCH 393/477] Apply ImRaii to UldWidget --- .../Windows/Data/Widgets/UldWidget.cs | 71 ++++++++----------- 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index 56ed45446..d2a195dc8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Memory; using Lumina.Data.Files; using Lumina.Data.Parsing.Uld; @@ -159,17 +160,19 @@ internal class UldWidget : IDataWindowWidget ImGuiColors.DalamudRed, $"Error: {nameof(UldFile.AssetData)} is not populated."); } - else if (ImGui.BeginTable("##uldTextureEntries"u8, 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders)) + else { - ImGui.TableSetupColumn("Id"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("000000"u8).X); - ImGui.TableSetupColumn("Path"u8, ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Actions"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Preview___"u8).X); - ImGui.TableHeadersRow(); + using var table = ImRaii.Table("##uldTextureEntries"u8, 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders); + if (table.Success) + { + ImGui.TableSetupColumn("Id"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("000000"u8).X); + ImGui.TableSetupColumn("Path"u8, ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Actions"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Preview___"u8).X); + ImGui.TableHeadersRow(); - foreach (var textureEntry in uld.AssetData) - this.DrawTextureEntry(textureEntry, textureManager); - - ImGui.EndTable(); + foreach (var textureEntry in uld.AssetData) + this.DrawTextureEntry(textureEntry, textureManager); + } } } @@ -283,7 +286,7 @@ internal class UldWidget : IDataWindowWidget if (ImGui.IsItemHovered()) { - ImGui.BeginTooltip(); + using var tooltip = ImRaii.Tooltip(); var texturePath = GetStringNullTerminated(textureEntry.Path); ImGui.Text($"Base path at {texturePath}:"); @@ -301,8 +304,6 @@ internal class UldWidget : IDataWindowWidget else if (e is not null) ImGui.Text(e.ToString()); } - - ImGui.EndTooltip(); } } @@ -311,15 +312,14 @@ internal class UldWidget : IDataWindowWidget ImGui.SliderInt("FrameData"u8, ref this.selectedFrameData, 0, timeline.FrameData.Length - 1); var frameData = timeline.FrameData[this.selectedFrameData]; ImGui.Text($"FrameInfo: {frameData.StartFrame} -> {frameData.EndFrame}"); - ImGui.Indent(); + + using var indent = ImRaii.PushIndent(); foreach (var frameDataKeyGroup in frameData.KeyGroups) { ImGui.Text($"{frameDataKeyGroup.Usage:G} {frameDataKeyGroup.Type:G}"); foreach (var keyframe in frameDataKeyGroup.Frames) this.DrawTimelineKeyGroupFrame(keyframe); } - - ImGui.Unindent(); } private void DrawTimelineKeyGroupFrame(IKeyframe frame) @@ -327,8 +327,7 @@ internal class UldWidget : IDataWindowWidget switch (frame) { case BaseKeyframeData baseKeyframeData: - ImGui.Text( - $"Time: {baseKeyframeData.Time} | Interpolation: {baseKeyframeData.Interpolation} | Acceleration: {baseKeyframeData.Acceleration} | Deceleration: {baseKeyframeData.Deceleration}"); + ImGui.Text($"Time: {baseKeyframeData.Time} | Interpolation: {baseKeyframeData.Interpolation} | Acceleration: {baseKeyframeData.Acceleration} | Deceleration: {baseKeyframeData.Deceleration}"); break; case Float1Keyframe float1Keyframe: this.DrawTimelineKeyGroupFrame(float1Keyframe.Keyframe); @@ -343,8 +342,7 @@ internal class UldWidget : IDataWindowWidget case Float3Keyframe float3Keyframe: this.DrawTimelineKeyGroupFrame(float3Keyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | Value1: {float3Keyframe.Value[0]} | Value2: {float3Keyframe.Value[1]} | Value3: {float3Keyframe.Value[2]}"); + ImGui.Text($" | Value1: {float3Keyframe.Value[0]} | Value2: {float3Keyframe.Value[1]} | Value3: {float3Keyframe.Value[2]}"); break; case SByte1Keyframe sbyte1Keyframe: this.DrawTimelineKeyGroupFrame(sbyte1Keyframe.Keyframe); @@ -359,8 +357,7 @@ internal class UldWidget : IDataWindowWidget case SByte3Keyframe sbyte3Keyframe: this.DrawTimelineKeyGroupFrame(sbyte3Keyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | Value1: {sbyte3Keyframe.Value[0]} | Value2: {sbyte3Keyframe.Value[1]} | Value3: {sbyte3Keyframe.Value[2]}"); + ImGui.Text($" | Value1: {sbyte3Keyframe.Value[0]} | Value2: {sbyte3Keyframe.Value[1]} | Value3: {sbyte3Keyframe.Value[2]}"); break; case Byte1Keyframe byte1Keyframe: this.DrawTimelineKeyGroupFrame(byte1Keyframe.Keyframe); @@ -375,8 +372,7 @@ internal class UldWidget : IDataWindowWidget case Byte3Keyframe byte3Keyframe: this.DrawTimelineKeyGroupFrame(byte3Keyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | Value1: {byte3Keyframe.Value[0]} | Value2: {byte3Keyframe.Value[1]} | Value3: {byte3Keyframe.Value[2]}"); + ImGui.Text($" | Value1: {byte3Keyframe.Value[0]} | Value2: {byte3Keyframe.Value[1]} | Value3: {byte3Keyframe.Value[2]}"); break; case Short1Keyframe short1Keyframe: this.DrawTimelineKeyGroupFrame(short1Keyframe.Keyframe); @@ -391,8 +387,7 @@ internal class UldWidget : IDataWindowWidget case Short3Keyframe short3Keyframe: this.DrawTimelineKeyGroupFrame(short3Keyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | Value1: {short3Keyframe.Value[0]} | Value2: {short3Keyframe.Value[1]} | Value3: {short3Keyframe.Value[2]}"); + ImGui.Text($" | Value1: {short3Keyframe.Value[0]} | Value2: {short3Keyframe.Value[1]} | Value3: {short3Keyframe.Value[2]}"); break; case UShort1Keyframe ushort1Keyframe: this.DrawTimelineKeyGroupFrame(ushort1Keyframe.Keyframe); @@ -407,8 +402,7 @@ internal class UldWidget : IDataWindowWidget case UShort3Keyframe ushort3Keyframe: this.DrawTimelineKeyGroupFrame(ushort3Keyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | Value1: {ushort3Keyframe.Value[0]} | Value2: {ushort3Keyframe.Value[1]} | Value3: {ushort3Keyframe.Value[2]}"); + ImGui.Text($" | Value1: {ushort3Keyframe.Value[0]} | Value2: {ushort3Keyframe.Value[1]} | Value3: {ushort3Keyframe.Value[2]}"); break; case Int1Keyframe int1Keyframe: this.DrawTimelineKeyGroupFrame(int1Keyframe.Keyframe); @@ -423,8 +417,7 @@ internal class UldWidget : IDataWindowWidget case Int3Keyframe int3Keyframe: this.DrawTimelineKeyGroupFrame(int3Keyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | Value1: {int3Keyframe.Value[0]} | Value2: {int3Keyframe.Value[1]} | Value3: {int3Keyframe.Value[2]}"); + ImGui.Text($" | Value1: {int3Keyframe.Value[0]} | Value2: {int3Keyframe.Value[1]} | Value3: {int3Keyframe.Value[2]}"); break; case UInt1Keyframe uint1Keyframe: this.DrawTimelineKeyGroupFrame(uint1Keyframe.Keyframe); @@ -439,8 +432,7 @@ internal class UldWidget : IDataWindowWidget case UInt3Keyframe uint3Keyframe: this.DrawTimelineKeyGroupFrame(uint3Keyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | Value1: {uint3Keyframe.Value[0]} | Value2: {uint3Keyframe.Value[1]} | Value3: {uint3Keyframe.Value[2]}"); + ImGui.Text($" | Value1: {uint3Keyframe.Value[0]} | Value2: {uint3Keyframe.Value[1]} | Value3: {uint3Keyframe.Value[2]}"); break; case Bool1Keyframe bool1Keyframe: this.DrawTimelineKeyGroupFrame(bool1Keyframe.Keyframe); @@ -455,28 +447,22 @@ internal class UldWidget : IDataWindowWidget case Bool3Keyframe bool3Keyframe: this.DrawTimelineKeyGroupFrame(bool3Keyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | Value1: {bool3Keyframe.Value[0]} | Value2: {bool3Keyframe.Value[1]} | Value3: {bool3Keyframe.Value[2]}"); + ImGui.Text($" | Value1: {bool3Keyframe.Value[0]} | Value2: {bool3Keyframe.Value[1]} | Value3: {bool3Keyframe.Value[2]}"); break; case ColorKeyframe colorKeyframe: this.DrawTimelineKeyGroupFrame(colorKeyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | Add: {colorKeyframe.AddRed} {colorKeyframe.AddGreen} {colorKeyframe.AddBlue} | Multiply: {colorKeyframe.MultiplyRed} {colorKeyframe.MultiplyGreen} {colorKeyframe.MultiplyBlue}"); + ImGui.Text($" | Add: {colorKeyframe.AddRed} {colorKeyframe.AddGreen} {colorKeyframe.AddBlue} | Multiply: {colorKeyframe.MultiplyRed} {colorKeyframe.MultiplyGreen} {colorKeyframe.MultiplyBlue}"); break; case LabelKeyframe labelKeyframe: this.DrawTimelineKeyGroupFrame(labelKeyframe.Keyframe); ImGui.SameLine(0, 0); - ImGui.Text( - $" | LabelCommand: {labelKeyframe.LabelCommand} | JumpId: {labelKeyframe.JumpId} | LabelId: {labelKeyframe.LabelId}"); + ImGui.Text($" | LabelCommand: {labelKeyframe.LabelCommand} | JumpId: {labelKeyframe.JumpId} | LabelId: {labelKeyframe.LabelId}"); break; } } - private void DrawParts( - UldRoot.PartsData partsData, - UldRoot.TextureEntry[] textureEntries, - TextureManager textureManager) + private void DrawParts(UldRoot.PartsData partsData, UldRoot.TextureEntry[] textureEntries, TextureManager textureManager) { for (var index = 0; index < partsData.Parts.Length; index++) { @@ -542,10 +528,9 @@ internal class UldWidget : IDataWindowWidget if (ImGui.IsItemHovered()) { - ImGui.BeginTooltip(); + using var tooltip = ImRaii.Tooltip(); ImGui.Text("Click to copy:"u8); ImGui.Text(texturePath); - ImGui.EndTooltip(); } } } From 8c26d6773965eb9a927b66657e68b913d3e08e49 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 3 Jan 2026 21:24:57 +0100 Subject: [PATCH 394/477] Apply ImRaii to TexWidget --- .../Windows/Data/Widgets/TexWidget.cs | 169 ++++++++---------- 1 file changed, 72 insertions(+), 97 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 3416a2506..747e18042 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -14,6 +14,7 @@ using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Internal; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -28,6 +29,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; ///
internal class TexWidget : IDataWindowWidget { + private const ImGuiTableFlags TableFlags = ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate | ImGuiTableFlags.SortMulti | + ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable | ImGuiTableFlags.NoBordersInBodyUntilResize | + ImGuiTableFlags.NoSavedSettings; + // TODO: move tracking implementation to PluginStats where applicable, // and show stats over there instead of TexWidget. private static readonly Dictionary< @@ -43,12 +48,12 @@ internal class TexWidget : IDataWindowWidget [DrawBlameTableColumnUserId.NativeAddress] = static x => x.ResourceAddress, }; - private readonly List addedTextures = new(); + private readonly List addedTextures = []; private string allLoadedTexturesTableName = "##table"; private string iconId = "18"; private bool hiRes = true; - private bool hq = false; + private bool hq; private string inputTexPath = string.Empty; private string inputFilePath = string.Empty; private Assembly[]? inputManifestResourceAssemblyCandidates; @@ -83,7 +88,7 @@ internal class TexWidget : IDataWindowWidget } /// - public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; + public string[]? CommandShortcuts { get; init; } = ["tex", "texture"]; /// public string DisplayName { get; init; } = "Tex"; @@ -140,45 +145,39 @@ internal class TexWidget : IDataWindowWidget var allBlames = this.textureManager.BlameTracker; lock (allBlames) { - ImGui.PushID("blames"u8); + using var pushedId = ImRaii.PushId("blames"u8); var sizeSum = allBlames.Sum(static x => Math.Max(0, x.RawSpecs.EstimatedBytes)); - if (ImGui.CollapsingHeader( - $"All Loaded Textures: {allBlames.Count:n0} ({Util.FormatBytes(sizeSum)})###header")) + if (ImGui.CollapsingHeader($"All Loaded Textures: {allBlames.Count:n0} ({Util.FormatBytes(sizeSum)})###header")) this.DrawBlame(allBlames); - ImGui.PopID(); } - ImGui.PushID("loadedGameTextures"u8); - if (ImGui.CollapsingHeader( - $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:n0}###header")) - this.DrawLoadedTextures(this.textureManager.Shared.ForDebugGamePathTextures); - ImGui.PopID(); + using (ImRaii.PushId("loadedGameTextures"u8)) + { + if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:n0}###header")) + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugGamePathTextures); + } - ImGui.PushID("loadedFileTextures"u8); - if (ImGui.CollapsingHeader( - $"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:n0}###header")) - this.DrawLoadedTextures(this.textureManager.Shared.ForDebugFileSystemTextures); - ImGui.PopID(); + using (ImRaii.PushId("loadedFileTextures"u8)) + { + if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:n0}###header")) + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugFileSystemTextures); + } - ImGui.PushID("loadedManifestResourceTextures"u8); - if (ImGui.CollapsingHeader( - $"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:n0}###header")) - this.DrawLoadedTextures(this.textureManager.Shared.ForDebugManifestResourceTextures); - ImGui.PopID(); + using (ImRaii.PushId("loadedManifestResourceTextures"u8)) + { + if (ImGui.CollapsingHeader($"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:n0}###header")) + this.DrawLoadedTextures(this.textureManager.Shared.ForDebugManifestResourceTextures); + } lock (this.textureManager.Shared.ForDebugInvalidatedTextures) { - ImGui.PushID("invalidatedTextures"u8); - if (ImGui.CollapsingHeader( - $"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:n0}###header")) - { + using var pushedId = ImRaii.PushId("invalidatedTextures"u8); + if (ImGui.CollapsingHeader($"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:n0}###header")) this.DrawLoadedTextures(this.textureManager.Shared.ForDebugInvalidatedTextures); - } - - ImGui.PopID(); } - ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + var textHeightSpacing = new Vector2(ImGui.GetTextLineHeightWithSpacing()); + ImGui.Dummy(textHeightSpacing); if (!this.textureManager.HasClipboardImage()) { @@ -191,59 +190,53 @@ internal class TexWidget : IDataWindowWidget if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon))) { - ImGui.PushID(nameof(this.DrawGetFromGameIcon)); + using var pushedId = ImRaii.PushId(nameof(this.DrawGetFromGameIcon)); this.DrawGetFromGameIcon(); - ImGui.PopID(); } if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGame))) { - ImGui.PushID(nameof(this.DrawGetFromGame)); + using var pushedId = ImRaii.PushId(nameof(this.DrawGetFromGame)); this.DrawGetFromGame(); - ImGui.PopID(); } if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromFile))) { - ImGui.PushID(nameof(this.DrawGetFromFile)); + using var pushedId = ImRaii.PushId(nameof(this.DrawGetFromFile)); this.DrawGetFromFile(); - ImGui.PopID(); } if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromManifestResource))) { - ImGui.PushID(nameof(this.DrawGetFromManifestResource)); + using var pushedId = ImRaii.PushId(nameof(this.DrawGetFromManifestResource)); this.DrawGetFromManifestResource(); - ImGui.PopID(); } if (ImGui.CollapsingHeader(nameof(ITextureProvider.CreateFromImGuiViewportAsync))) { - ImGui.PushID(nameof(this.DrawCreateFromImGuiViewportAsync)); + using var pushedId = ImRaii.PushId(nameof(this.DrawCreateFromImGuiViewportAsync)); this.DrawCreateFromImGuiViewportAsync(); - ImGui.PopID(); } if (ImGui.CollapsingHeader("UV"u8)) { - ImGui.PushID(nameof(this.DrawUvInput)); + using var pushedId = ImRaii.PushId(nameof(this.DrawUvInput)); this.DrawUvInput(); - ImGui.PopID(); } if (ImGui.CollapsingHeader($"CropCopy##{nameof(this.DrawExistingTextureModificationArgs)}")) { - ImGui.PushID(nameof(this.DrawExistingTextureModificationArgs)); + using var pushedId = ImRaii.PushId(nameof(this.DrawExistingTextureModificationArgs)); this.DrawExistingTextureModificationArgs(); - ImGui.PopID(); } - ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + ImGui.Dummy(textHeightSpacing); Action? runLater = null; foreach (var t in this.addedTextures) { - ImGui.PushID(t.Id); + using var pushedId = ImRaii.PushId(t.Id); + if (ImGui.CollapsingHeader($"Tex #{t.Id} {t}###header", ImGuiTreeNodeFlags.DefaultOpen)) { if (ImGui.Button("X"u8)) @@ -335,8 +328,6 @@ internal class TexWidget : IDataWindowWidget ImGui.Text(e.ToString()); } } - - ImGui.PopID(); } runLater?.Invoke(); @@ -356,18 +347,16 @@ internal class TexWidget : IDataWindowWidget if (ImGui.Button("Reset Columns"u8)) this.allLoadedTexturesTableName = "##table" + Environment.TickCount64; - if (!ImGui.BeginTable( - this.allLoadedTexturesTableName, - (int)DrawBlameTableColumnUserId.ColumnCount, - ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate | ImGuiTableFlags.SortMulti | - ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable | ImGuiTableFlags.NoBordersInBodyUntilResize | - ImGuiTableFlags.NoSavedSettings)) + using var table = ImRaii.Table(this.allLoadedTexturesTableName, (int)DrawBlameTableColumnUserId.ColumnCount, TableFlags); + if (!table.Success) return; const int numIcons = 1; float iconWidths; using (im.IconFontHandle?.Push()) + { iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.ToIconString()).X; + } ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupColumn( @@ -462,7 +451,8 @@ internal class TexWidget : IDataWindowWidget { var wrap = allBlames[i]; ImGui.TableNextRow(); - ImGui.PushID(i); + + using var pushedId = ImRaii.PushId(i); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -479,9 +469,8 @@ internal class TexWidget : IDataWindowWidget if (ImGui.IsItemHovered()) { - ImGui.BeginTooltip(); + using var tooltip = ImRaii.Tooltip(); ImGui.Image(wrap.Handle, wrap.Size); - ImGui.EndTooltip(); } ImGui.TableNextColumn(); @@ -503,21 +492,19 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); lock (wrap.OwnerPlugins) this.TextColumnCopiable(string.Join(", ", wrap.OwnerPlugins.Select(static x => x.Name)), false, true); - - ImGui.PopID(); } } clipper.Destroy(); - ImGui.EndTable(); ImGuiHelpers.ScaledDummy(10); } - private unsafe void DrawLoadedTextures(ICollection textures) + private void DrawLoadedTextures(ICollection textures) { var im = Service.Get(); - if (!ImGui.BeginTable("##table"u8, 6)) + using var table = ImRaii.Table("##table"u8, 6); + if (!table.Success) return; const int numIcons = 4; @@ -575,7 +562,7 @@ internal class TexWidget : IDataWindowWidget } var remain = texture.SelfReferenceExpiresInForDebug; - ImGui.PushID(row); + using var pushedId = ImRaii.PushId(row); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -602,28 +589,26 @@ internal class TexWidget : IDataWindowWidget if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate) { - ImGui.BeginTooltip(); + using var tooltip = ImRaii.Tooltip(); ImGui.Image(immediate.Handle, immediate.Size); - ImGui.EndTooltip(); } ImGui.SameLine(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Sync)) - this.textureManager.InvalidatePaths(new[] { texture.SourcePathForDebug }); + this.textureManager.InvalidatePaths([texture.SourcePathForDebug]); + if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Call {nameof(ITextureSubstitutionProvider.InvalidatePaths)}."); ImGui.SameLine(); - if (remain <= 0) - ImGui.BeginDisabled(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) - texture.ReleaseSelfReference(true); - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip("Release self-reference immediately."u8); - if (remain <= 0) - ImGui.EndDisabled(); + using (ImRaii.Disabled(remain <= 0)) + { + if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) + texture.ReleaseSelfReference(true); - ImGui.PopID(); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Release self-reference immediately."u8); + } } if (!valid) @@ -632,7 +617,6 @@ internal class TexWidget : IDataWindowWidget } clipper.Destroy(); - ImGui.EndTable(); ImGuiHelpers.ScaledDummy(10); } @@ -751,10 +735,7 @@ internal class TexWidget : IDataWindowWidget { ImGui.SameLine(); if (ImGui.Button("Load File (Async)"u8)) - { - this.addedTextures.Add( - new(Api10: this.textureManager.Shared.GetFromManifestResource(assembly, name).RentAsync())); - } + this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromManifestResource(assembly, name).RentAsync())); ImGui.SameLine(); if (ImGui.Button("Load File (Immediate)"u8)) @@ -767,21 +748,20 @@ internal class TexWidget : IDataWindowWidget private void DrawCreateFromImGuiViewportAsync() { var viewports = ImGui.GetPlatformIO().Viewports; - if (ImGui.BeginCombo( - nameof(this.viewportTextureArgs.ViewportId), - $"{this.viewportIndexInt}. {viewports[this.viewportIndexInt].ID:X08}")) + using (var combo = ImRaii.Combo(nameof(this.viewportTextureArgs.ViewportId), $"{this.viewportIndexInt}. {viewports[this.viewportIndexInt].ID:X08}")) { - for (var i = 0; i < viewports.Size; i++) + if (combo.Success) { - var sel = this.viewportIndexInt == i; - if (ImGui.Selectable($"#{i}: {viewports[i].ID:X08}", ref sel)) + for (var i = 0; i < viewports.Size; i++) { - this.viewportIndexInt = i; - ImGui.SetItemDefaultFocus(); + var sel = this.viewportIndexInt == i; + if (ImGui.Selectable($"#{i}: {viewports[i].ID:X08}", ref sel)) + { + this.viewportIndexInt = i; + ImGui.SetItemDefaultFocus(); + } } } - - ImGui.EndCombo(); } var b = this.viewportTextureArgs.KeepTransparency; @@ -843,17 +823,12 @@ internal class TexWidget : IDataWindowWidget } this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray(); - ImGui.Combo( - nameof(this.textureModificationArgs.DxgiFormat), - ref this.renderTargetChoiceInt, - this.supportedRenderTargetFormatNames); + ImGui.Combo(nameof(this.textureModificationArgs.DxgiFormat), ref this.renderTargetChoiceInt, this.supportedRenderTargetFormatNames); Span wh = stackalloc int[2]; wh[0] = this.textureModificationArgs.NewWidth; wh[1] = this.textureModificationArgs.NewHeight; - if (ImGui.InputInt( - $"{nameof(this.textureModificationArgs.NewWidth)}/{nameof(this.textureModificationArgs.NewHeight)}", - wh)) + if (ImGui.InputInt($"{nameof(this.textureModificationArgs.NewWidth)}/{nameof(this.textureModificationArgs.NewHeight)}", wh)) { this.textureModificationArgs.NewWidth = wh[0]; this.textureModificationArgs.NewHeight = wh[1]; From 5fe6df38876b289c70a39d7726cea131f8c8ae3b Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 3 Jan 2026 21:31:28 +0100 Subject: [PATCH 395/477] Cleanup TaskSchedulerWidget and ensure color is always popped --- .../Data/Widgets/TaskSchedulerWidget.cs | 45 +++++++------------ 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index cd72d751e..bf838623c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -35,7 +34,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget private CancellationTokenSource taskSchedulerCancelSource = new(); /// - public string[]? CommandShortcuts { get; init; } = { "tasksched", "taskscheduler" }; + public string[]? CommandShortcuts { get; init; } = ["tasksched", "taskscheduler"]; /// public string DisplayName { get; init; } = "Task Scheduler"; @@ -266,8 +265,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget ImGui.Text($"{this.downloadState.Downloaded:##,###}/{this.downloadState.Total:##,###} ({this.downloadState.Percentage:0.00}%)"); - using var disabled = - ImRaii.Disabled(this.downloadTask?.IsCompleted is false || this.localPath[0] == 0); + using var disabled = ImRaii.Disabled(this.downloadTask?.IsCompleted is false || this.localPath[0] == 0); ImGui.AlignTextToFramePadding(); ImGui.Text("Download"u8); ImGui.SameLine(); @@ -388,27 +386,19 @@ internal class TaskSchedulerWidget : IDataWindowWidget if (task.Task == null) subTime = task.FinishTime; - switch (task.Status) + using var pushedColor = task.Status switch { - case TaskStatus.Created: - case TaskStatus.WaitingForActivation: - case TaskStatus.WaitingToRun: - ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudGrey); - break; - case TaskStatus.Running: - case TaskStatus.WaitingForChildrenToComplete: - ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedBlue); - break; - case TaskStatus.RanToCompletion: - ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGreen); - break; - case TaskStatus.Canceled: - case TaskStatus.Faulted: - ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudRed); - break; - default: - throw new ArgumentOutOfRangeException(); - } + TaskStatus.Created or TaskStatus.WaitingForActivation or TaskStatus.WaitingToRun + => ImRaii.PushColor(ImGuiCol.Header, ImGuiColors.DalamudGrey), + TaskStatus.Running or TaskStatus.WaitingForChildrenToComplete + => ImRaii.PushColor(ImGuiCol.Header, ImGuiColors.ParsedBlue), + TaskStatus.RanToCompletion + => ImRaii.PushColor(ImGuiCol.Header, ImGuiColors.ParsedGreen), + TaskStatus.Canceled or TaskStatus.Faulted + => ImRaii.PushColor(ImGuiCol.Header, ImGuiColors.DalamudRed), + + _ => throw new ArgumentOutOfRangeException(), + }; if (ImGui.CollapsingHeader($"#{task.Id} - {task.Status} {(subTime - task.StartTime).TotalMilliseconds}ms###task{i}")) { @@ -418,8 +408,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget { try { - var cancelFunc = - typeof(Task).GetMethod("InternalCancel", BindingFlags.NonPublic | BindingFlags.Instance); + var cancelFunc = typeof(Task).GetMethod("InternalCancel", BindingFlags.NonPublic | BindingFlags.Instance); cancelFunc?.Invoke(task, null); } catch (Exception ex) @@ -430,7 +419,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); - ImGui.Text(task.StackTrace?.ToString()); + ImGui.Text(task.StackTrace?.ToString() ?? "Null StackTrace"); if (task.Exception != null) { @@ -443,8 +432,6 @@ internal class TaskSchedulerWidget : IDataWindowWidget { task.IsBeingViewed = false; } - - ImGui.PopStyleColor(1); } this.fileDialogManager.Draw(); From 09a1fd19255beb34a77f1b4123be9affacb510c0 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 3 Jan 2026 21:43:12 +0100 Subject: [PATCH 396/477] - Apply ImRaii to SeStringRendererTestWidget - Add new themes - Remove uses of Dalamud SeString - Use collection expression --- .../Widgets/SeStringRendererTestWidget.cs | 34 ++++++++----------- .../Windows/Data/Widgets/StartInfoWidget.cs | 2 +- .../Windows/Data/Widgets/TargetWidget.cs | 2 +- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs index 6a07152e5..958f9d037 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Internal; +using Dalamud.Interface.Utility.Raii; using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -27,7 +28,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal unsafe class SeStringRendererTestWidget : IDataWindowWidget { - private static readonly string[] ThemeNames = ["Dark", "Light", "Classic FF", "Clear Blue"]; + private static readonly string[] ThemeNames = ["Dark", "Light", "Classic FF", "Clear Blue", "Clear White", "Clear Green"]; private ImVectorWrapper testStringBuffer; private string testString = string.Empty; private ReadOnlySeString? logkind; @@ -117,9 +118,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget ImGui.SameLine(); var t4 = this.style.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType; - ImGui.PushItemWidth(ImGui.CalcTextSize("WWWWWWWWWWWWWW"u8).X); - if (ImGui.Combo("##theme", ref t4, ThemeNames)) - this.style.ThemeIndex = t4; + using (ImRaii.ItemWidth(ImGui.CalcTextSize("WWWWWWWWWWWWWW"u8).X)) + { + if (ImGui.Combo("##theme", ref t4, ThemeNames)) + this.style.ThemeIndex = t4; + } ImGui.SameLine(); t = this.style.LinkUnderlineThickness > 0f; @@ -190,22 +193,19 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget dl.PushClipRect(clipMin, clipMax); ImGuiHelpers.CompileSeStringWrapped( "Test test", - new SeStringDrawParams - { Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = dl }); + new SeStringDrawParams { Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = dl }); dl.PopClipRect(); } if (ImGui.CollapsingHeader("Addon Table"u8)) { - if (ImGui.BeginTable("Addon Sheet"u8, 3)) + using var table = ImRaii.Table("Addon Sheet"u8, 3); + if (table.Success) { ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupColumn("Row ID"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("0000000"u8).X); ImGui.TableSetupColumn("Text"u8, ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn( - "Misc"u8, - ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("AAAAAAAAAAAAAAAAA"u8).X); + ImGui.TableSetupColumn("Misc"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("AAAAAAAAAAAAAAAAA"u8).X); ImGui.TableHeadersRow(); var addon = Service.GetNullable()?.GetExcelSheet() ?? @@ -220,7 +220,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget var row = addon.GetRowAt(i); ImGui.TableNextRow(); - ImGui.PushID(i); + using var pushedId = ImRaii.PushId(i); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -232,14 +232,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget ImGui.TableNextColumn(); if (ImGui.Button("Print to Chat"u8)) - Service.Get().Print(row.Text.ToDalamudString()); - - ImGui.PopID(); + Service.Get().Print(row.Text); } } clipper.Destroy(); - ImGui.EndTable(); } } @@ -256,9 +253,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget if (ImGui.Button("Print to Chat Log"u8)) { - Service.Get().Print( - Game.Text.SeStringHandling.SeString.Parse( - Service.Get().CompileAndCache(this.testString).Data.Span)); + Service.Get().Print(Service.Get().CompileAndCache(this.testString)); } ImGui.SameLine(); @@ -313,6 +308,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); if (len + 4 >= this.testStringBuffer.Capacity) this.testStringBuffer.EnsureCapacityExponential(len + 4); + if (len < this.testStringBuffer.Capacity) { this.testStringBuffer.LengthUnsafe = len; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs index 7fb2cc2bf..4f71973c8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs @@ -9,7 +9,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class StartInfoWidget : IDataWindowWidget { /// - public string[]? CommandShortcuts { get; init; } = { "startinfo" }; + public string[]? CommandShortcuts { get; init; } = ["startinfo"]; /// public string DisplayName { get; init; } = "Start Info"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index 6caf3286d..2e52d7586 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -14,7 +14,7 @@ internal class TargetWidget : IDataWindowWidget private bool resolveGameData; /// - public string[]? CommandShortcuts { get; init; } = { "target" }; + public string[]? CommandShortcuts { get; init; } = ["target"]; /// public string DisplayName { get; init; } = "Target"; From 9b55b020ca32b0482b1306aebbbd6701ccd5fd47 Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Sat, 3 Jan 2026 23:11:50 +0100 Subject: [PATCH 397/477] Switch selftest to using mounts instead of teleporting --- .../SelfTest/Steps/ChatSelfTestStep.cs | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs index 16fd3b01e..73f0405ef 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs @@ -1,10 +1,13 @@ using Dalamud.Bindings.ImGui; +using Dalamud.Data; using Dalamud.Game.Chat; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.SelfTest; +using Lumina.Excel.Sheets; + namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; /// @@ -15,10 +18,10 @@ internal class ChatSelfTestStep : ISelfTestStep private int step = 0; private bool subscribedChatMessage = false; private bool subscribedLogMessage = false; - private bool hasPassed = false; - private bool hasTeleportGil = false; - private bool hasTeleportTicket = false; - private int teleportCount = 0; + private bool hasSeenEchoMessage = false; + private bool hasSeenMountMessage = false; + private string mountName = ""; + private string mountUser = ""; /// public string Name => "Test Chat"; @@ -45,7 +48,7 @@ internal class ChatSelfTestStep : ISelfTestStep chatGui.ChatMessage += this.ChatOnOnChatMessage; } - if (this.hasPassed) + if (this.hasSeenEchoMessage) { chatGui.ChatMessage -= this.ChatOnOnChatMessage; this.subscribedChatMessage = false; @@ -55,7 +58,7 @@ internal class ChatSelfTestStep : ISelfTestStep break; case 2: - ImGui.Text("Teleport somewhere..."); + ImGui.Text("Use any mount..."); if (!this.subscribedLogMessage) { @@ -63,18 +66,11 @@ internal class ChatSelfTestStep : ISelfTestStep chatGui.LogMessage += this.ChatOnLogMessage; } - if (this.hasTeleportGil) + if (this.hasSeenMountMessage) { - ImGui.Text($"You spent {this.teleportCount} gil to teleport."); - } - if (this.hasTeleportTicket) - { - ImGui.Text($"You used a ticket to teleport and have {this.teleportCount} remaining."); - } - - if (this.hasTeleportGil || this.hasTeleportTicket) - { - ImGui.Text("Is this correct?"); + ImGui.Text($"{this.mountUser} mounted {this.mountName}."); + + ImGui.Text("Is this correct? It is correct if this triggers on other players around you."); if (ImGui.Button("Yes")) { @@ -117,23 +113,25 @@ internal class ChatSelfTestStep : ISelfTestStep { if (type == XivChatType.Echo && message.TextValue == "DALAMUD") { - this.hasPassed = true; + this.hasSeenEchoMessage = true; } } private void ChatOnLogMessage(ILogMessage message) { - if (message.LogMessageId == 4590 && message.TryGetIntParameter(0, out var value)) + if (message.LogMessageId == 646 && message.TryGetIntParameter(0, out var value)) { - this.hasTeleportGil = true; - this.hasTeleportTicket = false; - this.teleportCount = value; - } - if (message.LogMessageId == 4591 && message.TryGetIntParameter(0, out var item) && item == 7569 && message.TryGetIntParameter(1, out var remaining)) - { - this.hasTeleportGil = false; - this.hasTeleportTicket = true; - this.teleportCount = remaining; + this.hasSeenMountMessage = true; + this.mountUser = message.SourceEntity?.Name.ExtractText() ?? ""; + try + { + this.mountName = Service.Get().GetExcelSheet().GetRow((uint)value).Singular.ExtractText(); + } + catch + { + // ignore any errors with retrieving the mount name, they are probably not related to this test + this.mountName = $"Mount ID: {value} (failed to retrieve mount name)"; + } } } } From 790669e60abdc32e49e59c65315950340140b1a3 Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Sun, 4 Jan 2026 08:12:06 +0100 Subject: [PATCH 398/477] Battle log exists, selftest with the use action message --- .../SelfTest/Steps/ChatSelfTestStep.cs | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs index 73f0405ef..d4311176e 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs @@ -1,13 +1,10 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Data; using Dalamud.Game.Chat; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.SelfTest; -using Lumina.Excel.Sheets; - namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; /// @@ -19,9 +16,9 @@ internal class ChatSelfTestStep : ISelfTestStep private bool subscribedChatMessage = false; private bool subscribedLogMessage = false; private bool hasSeenEchoMessage = false; - private bool hasSeenMountMessage = false; - private string mountName = ""; - private string mountUser = ""; + private bool hasSeenActionMessage = false; + private string actionName = ""; + private string actionUser = ""; /// public string Name => "Test Chat"; @@ -58,7 +55,7 @@ internal class ChatSelfTestStep : ISelfTestStep break; case 2: - ImGui.Text("Use any mount..."); + ImGui.Text("Use any action (for example Sprint) or be near a player using an action."); if (!this.subscribedLogMessage) { @@ -66,11 +63,10 @@ internal class ChatSelfTestStep : ISelfTestStep chatGui.LogMessage += this.ChatOnLogMessage; } - if (this.hasSeenMountMessage) + if (this.hasSeenActionMessage) { - ImGui.Text($"{this.mountUser} mounted {this.mountName}."); - - ImGui.Text("Is this correct? It is correct if this triggers on other players around you."); + ImGui.Text($"{this.actionUser} used {this.actionName}."); + ImGui.Text("Is this correct?"); if (ImGui.Button("Yes")) { @@ -119,19 +115,11 @@ internal class ChatSelfTestStep : ISelfTestStep private void ChatOnLogMessage(ILogMessage message) { - if (message.LogMessageId == 646 && message.TryGetIntParameter(0, out var value)) + if (message.LogMessageId == 533 && message.TryGetStringParameter(0, out var value)) { - this.hasSeenMountMessage = true; - this.mountUser = message.SourceEntity?.Name.ExtractText() ?? ""; - try - { - this.mountName = Service.Get().GetExcelSheet().GetRow((uint)value).Singular.ExtractText(); - } - catch - { - // ignore any errors with retrieving the mount name, they are probably not related to this test - this.mountName = $"Mount ID: {value} (failed to retrieve mount name)"; - } + this.hasSeenActionMessage = true; + this.actionUser = message.SourceEntity?.Name.ExtractText() ?? ""; + this.actionName = value.ExtractText(); } } } From 6e19aca481a9c49ea0a46b94bdc8671c230f3f82 Mon Sep 17 00:00:00 2001 From: RedworkDE <10944644+RedworkDE@users.noreply.github.com> Date: Sun, 4 Jan 2026 16:00:10 +0100 Subject: [PATCH 399/477] Fix StyleCop warnings --- Dalamud/Game/Chat/LogMessage.cs | 75 ++++++++++--------- Dalamud/Game/Chat/LogMessageEntity.cs | 48 ++++++++---- .../SelfTest/Steps/ChatSelfTestStep.cs | 4 +- Dalamud/Plugin/Services/IChatGui.cs | 1 - 4 files changed, 72 insertions(+), 56 deletions(-) diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs index 93b928d48..c772783a1 100644 --- a/Dalamud/Game/Chat/LogMessage.cs +++ b/Dalamud/Game/Chat/LogMessage.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + using Dalamud.Data; using Dalamud.Utility; @@ -10,8 +12,6 @@ using FFXIVClientStructs.Interop; using Lumina.Excel; using Lumina.Text.ReadOnly; -using System.Diagnostics.CodeAnalysis; - namespace Dalamud.Game.Chat; /// @@ -88,30 +88,41 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa /// public RowRef GameData => LuminaUtils.CreateRef(ptr->LogMessageId); - public LogMessageEntity SourceEntity => new LogMessageEntity(ptr, true); /// ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity; - public LogMessageEntity TargetEntity => new LogMessageEntity(ptr, false); - /// ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity; /// public int ParameterCount => ptr->Parameters.Count; - public bool TryGetParameter(int index, out TextParameter value) - { - if (index < 0 || index >= ptr->Parameters.Count) - { - value = default; - return false; - } + private LogMessageEntity SourceEntity => new(ptr, true); - value = ptr->Parameters[index]; - return true; + private LogMessageEntity TargetEntity => new(ptr, false); + + public static bool operator ==(LogMessage x, LogMessage y) => x.Equals(y); + + public static bool operator !=(LogMessage x, LogMessage y) => !(x == y); + + /// + public bool Equals(ILogMessage? other) + { + return other is LogMessage logMessage && this.Equals(logMessage); } + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is LogMessage logMessage && this.Equals(logMessage); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.LogMessageId, this.SourceEntity, this.TargetEntity); + } + /// public bool TryGetIntParameter(int index, out int value) { @@ -132,6 +143,7 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa value = new(parameter.StringValue.AsSpan()); return true; } + if (parameter.Type == TextParameterType.ReferencedUtf8String) { value = new(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan()); @@ -157,7 +169,7 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa return new ReadOnlySeString(utf8.AsSpan()); - void SetName(RaptureLogModule* self, LogMessageEntity item) + static void SetName(RaptureLogModule* self, LogMessageEntity item) { var name = item.NameSpan.GetPointer(0); @@ -190,31 +202,20 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa } } + private bool TryGetParameter(int index, out TextParameter value) + { + if (index < 0 || index >= ptr->Parameters.Count) + { + value = default; + return false; + } - public static bool operator ==(LogMessage x, LogMessage y) => x.Equals(y); + value = ptr->Parameters[index]; + return true; + } - public static bool operator !=(LogMessage x, LogMessage y) => !(x == y); - - public bool Equals(LogMessage other) + private bool Equals(LogMessage other) { return this.LogMessageId == other.LogMessageId && this.SourceEntity == other.SourceEntity && this.TargetEntity == other.TargetEntity; } - - /// - public bool Equals(ILogMessage? other) - { - return other is LogMessage logMessage && this.Equals(logMessage); - } - - /// - public override bool Equals([NotNullWhen(true)] object? obj) - { - return obj is LogMessage logMessage && this.Equals(logMessage); - } - - /// - public override int GetHashCode() - { - return HashCode.Combine(this.LogMessageId, this.SourceEntity, this.TargetEntity); - } } diff --git a/Dalamud/Game/Chat/LogMessageEntity.cs b/Dalamud/Game/Chat/LogMessageEntity.cs index 4294e4898..91e905928 100644 --- a/Dalamud/Game/Chat/LogMessageEntity.cs +++ b/Dalamud/Game/Chat/LogMessageEntity.cs @@ -37,46 +37,57 @@ public interface ILogMessageEntity : IEquatable uint ObjStrId { get; } /// - /// Gets a boolean indicating if this entity is a player. + /// Gets a value indicating whether this entity is a player. /// bool IsPlayer { get; } } - /// /// This struct represents an entity related to a log message. /// /// A pointer to the log message item. -/// If represents the source entity of the log message, otherwise represents the target entity +/// If represents the source entity of the log message, otherwise represents the target entity. internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity { - public Span NameSpan => source ? ptr->SourceName : ptr->TargetName; - - public ReadOnlySeString Name => new ReadOnlySeString(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]); + /// + public ReadOnlySeString Name => new(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]); + /// public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld; + /// public RowRef HomeWorld => LuminaUtils.CreateRef(this.HomeWorldId); + /// public uint ObjStrId => source ? ptr->SourceObjStrId : ptr->TargetObjStrId; - public byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind; - - public byte Sex => source ? ptr->SourceSex : ptr->TargetSex; - + /// public bool IsPlayer => source ? ptr->SourceIsPlayer : ptr->TargetIsPlayer; - public bool IsSourceEntity => source; + /// + /// Gets the Span containing the raw name of this entity. + /// + internal Span NameSpan => source ? ptr->SourceName : ptr->TargetName; + + /// + /// Gets the kind of the entity. + /// + internal byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind; + + /// + /// Gets the Sex of this entity. + /// + internal byte Sex => source ? ptr->SourceSex : ptr->TargetSex; + + /// + /// Gets a value indicating whether this entity is the source entity of a log message. + /// + internal bool IsSourceEntity => source; public static bool operator ==(LogMessageEntity x, LogMessageEntity y) => x.Equals(y); public static bool operator !=(LogMessageEntity x, LogMessageEntity y) => !(x == y); - public bool Equals(LogMessageEntity other) - { - return this.Name == other.Name && this.HomeWorldId == other.HomeWorldId && this.ObjStrId == other.ObjStrId && this.Kind == other.Kind && this.Sex == other.Sex && this.IsPlayer == other.IsPlayer; - } - /// public bool Equals(ILogMessageEntity other) { @@ -94,4 +105,9 @@ internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool { return HashCode.Combine(this.Name, this.HomeWorldId, this.ObjStrId, this.Sex, this.IsPlayer); } + + private bool Equals(LogMessageEntity other) + { + return this.Name == other.Name && this.HomeWorldId == other.HomeWorldId && this.ObjStrId == other.ObjStrId && this.Kind == other.Kind && this.Sex == other.Sex && this.IsPlayer == other.IsPlayer; + } } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs index d4311176e..851957b4b 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs @@ -17,8 +17,8 @@ internal class ChatSelfTestStep : ISelfTestStep private bool subscribedLogMessage = false; private bool hasSeenEchoMessage = false; private bool hasSeenActionMessage = false; - private string actionName = ""; - private string actionUser = ""; + private string actionName = string.Empty; + private string actionUser = string.Empty; /// public string Name => "Test Chat"; diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs index eec25cb5a..2bb7b6913 100644 --- a/Dalamud/Plugin/Services/IChatGui.cs +++ b/Dalamud/Plugin/Services/IChatGui.cs @@ -51,7 +51,6 @@ public interface IChatGui : IDalamudService /// The message sent. public delegate void OnMessageUnhandledDelegate(XivChatType type, int timestamp, SeString sender, SeString message); - /// /// A delegate type used with the event. /// From 13980542168073087f38cf6da09516d8aa0d8ced Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 4 Jan 2026 14:03:15 -0800 Subject: [PATCH 400/477] Push AddonLifecycle event register/unregister to main thread --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 78cea1a0f..4da8e429c 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -27,6 +27,9 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private static readonly ModuleLog Log = new("AddonLifecycle"); + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] @@ -58,20 +61,23 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - if (!this.EventListeners.ContainsKey(listener.EventType)) + this.framework.RunOnFrameworkThread(() => { - if (!this.EventListeners.TryAdd(listener.EventType, [])) - return; - } + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + if (!this.EventListeners.TryAdd(listener.EventType, [])) + return; + } - // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type - if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) - { - if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) - return; - } + // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) + { + if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) + return; + } - this.EventListeners[listener.EventType][listener.AddonName].Add(listener); + this.EventListeners[listener.EventType][listener.AddonName].Add(listener); + }); } /// @@ -80,13 +86,16 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) + this.framework.RunOnFrameworkThread(() => { - if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) { - addonListener.Remove(listener); + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + { + addonListener.Remove(listener); + } } - } + }); } /// From 36c3429566478cffa01a2aa4677299d528c4b48b Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 4 Jan 2026 14:41:30 -0800 Subject: [PATCH 401/477] Force to next tick instead of running immediately --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 4da8e429c..d07821149 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -31,6 +31,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private readonly Framework framework = Service.Get(); private Hook? onInitializeAddonHook; + private bool isInvokingListeners = false; [ServiceManager.ServiceConstructor] private AddonLifecycle() @@ -61,7 +62,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnFrameworkThread(() => + this.framework.RunOnTick(() => { if (!this.EventListeners.ContainsKey(listener.EventType)) { @@ -77,7 +78,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } this.EventListeners[listener.EventType][listener.AddonName].Add(listener); - }); + }, delayTicks: this.isInvokingListeners ? 1 : 0); } /// @@ -86,7 +87,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnFrameworkThread(() => + this.framework.RunOnTick(() => { if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) { @@ -95,7 +96,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService addonListener.Remove(listener); } } - }); + }, delayTicks: this.isInvokingListeners ? 1 : 0); } /// @@ -106,6 +107,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// What to blame on errors. internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { + this.isInvokingListeners = true; + // Early return if we don't have any listeners of this type if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; @@ -140,6 +143,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } } } + + this.isInvokingListeners = false; } /// From bcc16c9b0e0d195162cbbbf38d67df9639719512 Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:00:31 +0100 Subject: [PATCH 402/477] Add CPU info to crash log (#2565) * Add CPU info to crash log Added a function to retrieve CPU vendor and brand information. * Add missing include * Remove unused std::strings --- DalamudCrashHandler/DalamudCrashHandler.cpp | 60 +++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 3955bd983..f28715dc1 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -689,6 +690,60 @@ void restart_game_using_injector(int nRadioButton, const std::vector cpui; + int nIds_; + int nExIds_; + std::vector> data_; + std::vector> extdata_; + size_t convertedChars = 0; + + // Calling __cpuid with 0x0 as the function_id argument + // gets the number of the highest valid function ID. + __cpuid(cpui.data(), 0); + nIds_ = cpui[0]; + + for (int i = 0; i <= nIds_; ++i) + { + __cpuidex(cpui.data(), i, 0); + data_.push_back(cpui); + } + + // Capture vendor string + char vendorA[0x20]; + memset(vendorA, 0, sizeof(vendorA)); + *reinterpret_cast(vendorA) = data_[0][1]; + *reinterpret_cast(vendorA + 4) = data_[0][3]; + *reinterpret_cast(vendorA + 8) = data_[0][2]; + mbstowcs_s(&convertedChars, vendor, 0x20, vendorA, _TRUNCATE); + + // Calling __cpuid with 0x80000000 as the function_id argument + // gets the number of the highest valid extended ID. + __cpuid(cpui.data(), 0x80000000); + nExIds_ = cpui[0]; + + for (int i = 0x80000000; i <= nExIds_; ++i) + { + __cpuidex(cpui.data(), i, 0); + extdata_.push_back(cpui); + } + + // Interpret CPU brand string if reported + if (nExIds_ >= 0x80000004) + { + char brandA[0x40]; + memset(brandA, 0, sizeof(brandA)); + memcpy(brandA, extdata_[2].data(), sizeof(cpui)); + memcpy(brandA + 16, extdata_[3].data(), sizeof(cpui)); + memcpy(brandA + 32, extdata_[4].data(), sizeof(cpui)); + mbstowcs_s(&convertedChars, brand, 0x40, brandA, _TRUNCATE); + } +} + int main() { enum crash_handler_special_exit_codes { UnknownError = -99, @@ -941,6 +996,9 @@ int main() { const bool is_external_event = exinfo.ExceptionRecord.ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT; std::wostringstream log; + wchar_t vendor[0x20]; + wchar_t brand[0x40]; + get_cpu_info(vendor, brand); if (!is_external_event) { @@ -962,6 +1020,8 @@ int main() { else log << std::format(L"Dump error: {}", dumpError) << std::endl; log << std::format(L"System Time: {0:%F} {0:%T} {0:%Ez}", std::chrono::system_clock::now()) << std::endl; + log << std::format(L"CPU Vendor: {}", vendor) << std::endl; + log << std::format(L"CPU Brand: {}", brand) << std::endl; log << L"\n" << stackTrace << std::endl; if (pProgressDialog) From bd05f4c1a5a630ed2082ee8d9b0d13a18fca18d7 Mon Sep 17 00:00:00 2001 From: Infi Date: Mon, 5 Jan 2026 03:15:01 +0100 Subject: [PATCH 403/477] - Switch SeString to ReadOnlySeString - Utf8String to ReadOnlySeString avoiding Marshal - Remove unnecessary ImRaii checks - Switch default to 0 --- .../UiDebug2/Browsing/AddonTree.AtkValues.cs | 3 - .../UiDebug2/Browsing/AddonTree.FieldNames.cs | 2 +- .../Internal/UiDebug2/Browsing/AddonTree.cs | 1 - .../Internal/UiDebug2/Browsing/Events.cs | 9 +- .../UiDebug2/Browsing/NodeTree.Component.cs | 16 ++-- .../UiDebug2/Browsing/NodeTree.Counter.cs | 4 +- .../UiDebug2/Browsing/NodeTree.Editor.cs | 23 ++--- .../UiDebug2/Browsing/NodeTree.Image.cs | 2 - .../UiDebug2/Browsing/NodeTree.NineGrid.cs | 9 +- .../UiDebug2/Browsing/NodeTree.Res.cs | 2 - .../UiDebug2/Browsing/NodeTree.Text.cs | 33 +++---- .../UiDebug2/Browsing/TimelineTree.cs | 4 - .../Internal/UiDebug2/ElementSelector.cs | 86 +++++++++---------- .../Internal/UiDebug2/Popout.Addon.cs | 2 +- .../Internal/UiDebug2/Popout.Node.cs | 4 +- .../Internal/UiDebug2/Utility/Gui.cs | 13 ++- .../Internal/UiDebug2/Utility/NodeBounds.cs | 20 ++--- 17 files changed, 104 insertions(+), 129 deletions(-) diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs index b31f74264..730c2c837 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs @@ -2,9 +2,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Internal.UiDebug2.Utility; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using Dalamud.Memory; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -28,7 +26,6 @@ public unsafe partial class AddonTree if (tree.Success) { using var tbl = ImRaii.Table("atkUnitBase_atkValueTable"u8, 3, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (tbl.Success) { ImGui.TableSetupColumn("Index"u8); diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs index 0b1dcb66c..88a9770aa 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs @@ -41,7 +41,7 @@ public unsafe partial class AddonTree { foreach (var t in from t in ClientStructsAssembly.GetTypes() where t.IsPublic - let xivAddonAttr = (AddonAttribute?)t.GetCustomAttribute(typeof(AddonAttribute), false) + let xivAddonAttr = t.GetCustomAttribute(false) where xivAddonAttr != null where xivAddonAttr.AddonIdentifiers.Contains(this.AddonName) select t) diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs index 7cb2cc704..2e0874206 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs @@ -4,7 +4,6 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; -using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs index 98c7d9efe..87c793d4e 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs @@ -28,11 +28,9 @@ public static class Events } using var tree = ImRaii.TreeNode($"Events##{(nint)node:X}eventTree"); - if (tree.Success) { using var tbl = ImRaii.Table($"##{(nint)node:X}eventTable", 7, Resizable | SizingFixedFit | Borders | RowBg); - if (tbl.Success) { ImGui.TableSetupColumn("#"u8, WidthFixed); @@ -50,18 +48,25 @@ public static class Events { ImGui.TableNextColumn(); ImGui.Text($"{i++}"); + ImGui.TableNextColumn(); ImGui.Text($"{evt->State.EventType}"); + ImGui.TableNextColumn(); ImGui.Text($"{evt->Param}"); + ImGui.TableNextColumn(); ImGui.Text($"{evt->State.StateFlags}"); + ImGui.TableNextColumn(); ImGui.Text($"{evt->State.ReturnFlags}"); + ImGui.TableNextColumn(); ImGuiHelpers.ClickToCopyText($"{(nint)evt->Target:X}", default, new Vector4(0.6f, 0.6f, 0.6f, 1)); + ImGui.TableNextColumn(); ImGuiHelpers.ClickToCopyText($"{(nint)evt->Listener:X}", default, new Vector4(0.6f, 0.6f, 0.6f, 1)); + evt = evt->NextEvent; } } diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs index 922d226b6..8f34f4be6 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs @@ -3,6 +3,8 @@ using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Component.GUI; +using Lumina.Text.ReadOnly; + using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; using static Dalamud.Utility.Util; using static FFXIVClientStructs.FFXIV.Component.GUI.ComponentType; @@ -89,14 +91,14 @@ internal unsafe class ComponentNodeTree : ResNodeTree { case TextInput: var textInputComponent = (AtkComponentTextInput*)this.Component; - ImGui.Text($"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.EvaluatedString.StringPtr))}"); - ImGui.Text($"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.RawString.StringPtr))}"); + ImGui.Text($"InputBase Text1 (Lumina): {new ReadOnlySeStringSpan(textInputComponent->AtkComponentInputBase.EvaluatedString.AsSpan()).ToMacroString()}"); + ImGui.Text($"InputBase Text2 (Lumina): {new ReadOnlySeStringSpan(textInputComponent->AtkComponentInputBase.RawString.AsSpan()).ToMacroString()}"); // TODO: Reenable when unknowns have been unprivated / named - // ImGui.Text($"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}"); - // ImGui.Text($"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}"); - ImGui.Text($"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}"); - ImGui.Text($"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}"); - ImGui.Text($"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}"); + // ImGui.Text($"Text1: {new ReadOnlySeStringSpan(textInputComponent->UnkText01.AsSpan()).ToMacroString()}"); + // ImGui.Text($"Text2: {new ReadOnlySeStringSpan(textInputComponent->UnkText02.AsSpan()).ToMacroString()}"); + ImGui.Text($"AvailableLines: {new ReadOnlySeStringSpan(textInputComponent->AvailableLines.AsSpan()).ToMacroString()}"); + ImGui.Text($"HighlightedAutoTranslateOptionColorPrefix: {new ReadOnlySeStringSpan(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.AsSpan()).ToMacroString()}"); + ImGui.Text($"HighlightedAutoTranslateOptionColorSuffix: {new ReadOnlySeStringSpan(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.AsSpan()).ToMacroString()}"); break; case List: case TreeList: diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs index ff40db37a..2b2adbcee 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs @@ -1,5 +1,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI; +using Lumina.Text.ReadOnly; + using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; using static Dalamud.Utility.Util; @@ -30,7 +32,7 @@ internal unsafe partial class CounterNodeTree : ResNodeTree { if (!isEditorOpen) { - PrintFieldValuePairs(("Text", ((AtkCounterNode*)this.Node)->NodeText.ToString())); + PrintFieldValuePairs(("Text", new ReadOnlySeStringSpan(((AtkCounterNode*)this.Node)->NodeText.AsSpan()).ToMacroString())); } } } diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs index ae6f5fffa..4d1f4bb62 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs @@ -7,6 +7,8 @@ using Dalamud.Interface.Internal.UiDebug2.Utility; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Component.GUI; +using Lumina.Text.ReadOnly; + using static Dalamud.Bindings.ImGui.ImGuiColorEditFlags; using static Dalamud.Bindings.ImGui.ImGuiInputTextFlags; using static Dalamud.Bindings.ImGui.ImGuiTableColumnFlags; @@ -27,10 +29,10 @@ internal unsafe partial class ResNodeTree private protected void DrawNodeEditorTable() { using var tbl = ImRaii.Table($"###Editor{(nint)this.Node}", 2, SizingStretchProp | NoHostExtendX); - if (tbl.Success) - { - this.DrawEditorRows(); - } + if (!tbl.Success) + return; + + this.DrawEditorRows(); } /// @@ -59,7 +61,7 @@ internal unsafe partial class ResNodeTree ImGui.TableNextColumn(); ImGui.SetNextItemWidth(150); - if (ImGui.DragFloat2($"##{(nint)this.Node:X}position", ref pos, 1, default, default, "%.0f")) + if (ImGui.DragFloat2($"##{(nint)this.Node:X}position", ref pos, 1, 0, 0, "%.0f")) { this.Node->X = pos.X; this.Node->Y = pos.Y; @@ -73,7 +75,7 @@ internal unsafe partial class ResNodeTree ImGui.Text("Size:"u8); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(150); - if (ImGui.DragFloat2($"##{(nint)this.Node:X}size", ref size, 1, 0, default, "%.0f")) + if (ImGui.DragFloat2($"##{(nint)this.Node:X}size", ref size, 1, 0, 0, "%.0f")) { this.Node->Width = (ushort)Math.Max(size.X, 0); this.Node->Height = (ushort)Math.Max(size.Y, 0); @@ -101,7 +103,7 @@ internal unsafe partial class ResNodeTree ImGui.Text("Origin:"u8); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(150); - if (ImGui.DragFloat2($"##{(nint)this.Node:X}origin", ref origin, 1, default, default, "%.0f")) + if (ImGui.DragFloat2($"##{(nint)this.Node:X}origin", ref origin, 1, 0, 0, "%.0f")) { this.Node->OriginX = origin.X; this.Node->OriginY = origin.Y; @@ -120,7 +122,7 @@ internal unsafe partial class ResNodeTree angle -= 360; } - if (ImGui.DragFloat($"##{(nint)this.Node:X}rotation", ref angle, 0.05f, default, default, "%.2f°")) + if (ImGui.DragFloat($"##{(nint)this.Node:X}rotation", ref angle, 0.05f, 0, 0, "%.2f°")) { this.Node->Rotation = (float)(angle / (180 / Math.PI)); this.Node->DrawFlags |= 0xD; @@ -168,7 +170,6 @@ internal unsafe partial class ResNodeTree ImGui.Text("Add:"u8); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(124); - if (ImGui.DragFloat3($"##{(nint)this.Node:X}addRGB", ref add, 1, -255, 255, "%.0f")) { this.Node->AddRed = (short)add.X; @@ -199,7 +200,7 @@ internal unsafe partial class CounterNodeTree { base.DrawEditorRows(); - var str = this.CntNode->NodeText.ToString(); + var str = new ReadOnlySeStringSpan(this.CntNode->NodeText.AsSpan()).ToMacroString(); ImGui.TableNextRow(); ImGui.TableNextColumn(); @@ -299,7 +300,7 @@ internal unsafe partial class TextNodeTree { base.DrawEditorRows(); - var text = this.TxtNode->NodeText.ToString(); + var text = new ReadOnlySeStringSpan(this.TxtNode->NodeText.AsSpan()).ToMacroString(); var fontIndex = FontList.IndexOf(this.TxtNode->FontType); int fontSize = this.TxtNode->FontSize; var alignment = this.TxtNode->AlignmentType; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs index 260ea4942..907261d36 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs @@ -2,7 +2,6 @@ using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -64,7 +63,6 @@ internal unsafe partial class ImageNodeTree : ResNodeTree } using var tree = ImRaii.TreeNode($"Texture##texture{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", SpanFullWidth); - if (tree.Success) { PrintFieldValuePairs( diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs index 489135ed0..1c06dfb40 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs @@ -1,6 +1,5 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface.Internal.UiDebug2.Utility; -using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -61,10 +60,10 @@ internal unsafe partial class NineGridNodeTree : ImageNodeTree var ngCol = RgbaVector4ToUint(col with { W = 0.75f * col.W }); - ImGui.GetWindowDrawList() - .AddRect(partBegin, partEnd, RgbaVector4ToUint(col)); - ImGui.GetWindowDrawList().AddRect(ngBegin1, ngEnd1, ngCol); - ImGui.GetWindowDrawList().AddRect(ngBegin2, ngEnd2, ngCol); + var windowDrawList = ImGui.GetWindowDrawList(); + windowDrawList.AddRect(partBegin, partEnd, RgbaVector4ToUint(col)); + windowDrawList.AddRect(ngBegin1, ngEnd1, ngCol); + windowDrawList.AddRect(ngBegin2, ngEnd2, ngCol); ImGui.SetCursorPos(cursorLocalPos + uv + new Vector2(0, -20)); ImGui.TextColored(col, $"[#{partId}]\t{part.U}, {part.V}\t{part.Width}x{part.Height}"); diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs index 418156811..4107ef53f 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.UiDebug2.Utility; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -138,7 +137,6 @@ internal unsafe partial class ResNodeTree : IDisposable PrintNodeList(nodeList, count, addonTree); var lineEnd = lineStart with { Y = ImGui.GetCursorScreenPos().Y - 7 }; - if (lineStart.Y < lineEnd.Y) { ImGui.GetWindowDrawList().AddLine(lineStart, lineEnd, RgbaVector4ToUint(color), 1); diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs index 1435335db..6d4fff85e 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs @@ -2,15 +2,14 @@ using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.ImGuiSeStringRenderer; -using Dalamud.Interface.Internal.UiDebug2.Utility; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Component.GUI; +using Lumina.Text.ReadOnly; + using static Dalamud.Interface.ColorHelpers; using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; using static Dalamud.Utility.Util; @@ -64,7 +63,7 @@ internal unsafe partial class TextNodeTree : ResNodeTree } catch { - ImGui.Text(Marshal.PtrToStringAnsi(new(this.NodeText.StringPtr)) ?? string.Empty); + ImGui.Text(new ReadOnlySeStringSpan(this.NodeText.AsSpan()).ToMacroString()); } PrintFieldValuePairs( @@ -82,36 +81,24 @@ internal unsafe partial class TextNodeTree : ResNodeTree private void PrintPayloads() { using var tree = ImRaii.TreeNode($"Text Payloads##{(nint)this.Node:X}"); - if (tree.Success) { - var utf8String = this.NodeText; - var seStringBytes = new byte[utf8String.BufUsed]; - for (var i = 0L; i < utf8String.BufUsed; i++) + var idx = 0; + foreach (var payload in new ReadOnlySeString(this.NodeText.AsSpan())) { - seStringBytes[i] = utf8String.StringPtr.Value[i]; - } - - var seString = SeString.Parse(seStringBytes); - for (var i = 0; i < seString.Payloads.Count; i++) - { - var payload = seString.Payloads[i]; - ImGui.Text($"[{i}]"); + ImGui.Text($"[{idx}]"); ImGui.SameLine(); switch (payload.Type) { - case PayloadType.RawText when payload is TextPayload tp: - { - Gui.PrintFieldValuePair("Raw Text", tp.Text ?? string.Empty); + case ReadOnlySePayloadType.Text: + PrintFieldValuePair("Raw Text", payload.ToString()); break; - } - default: - { ImGui.Text(payload.ToString()); break; - } } + + idx++; } } } diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs index 21f4fb54a..3545bc2f1 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs @@ -57,7 +57,6 @@ public readonly unsafe partial struct TimelineTree if (animationCount > 0) { using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth); - if (tree.Success) { PrintFieldValuePair("Timeline", $"{(nint)this.NodeTimeline:X}"); @@ -89,7 +88,6 @@ public readonly unsafe partial struct TimelineTree if (labelSetCount > 0 && this.Resource->LabelSets is not null) { using var tree = ImRaii.TreeNode($"Timeline Label Sets##{(nint)this.node:X}LabelSets", SpanFullWidth); - if (tree.Success) { this.DrawLabelSets(); @@ -324,7 +322,6 @@ public readonly unsafe partial struct TimelineTree using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 0.65F, 0.4F, 1), isActive)) { using var tree = ImRaii.TreeNode($"[#{a}] [Frames {animation.StartFrameIdx}-{animation.EndFrameIdx}] {(isActive ? " (Active)" : string.Empty)}###{(nint)this.node}animTree{a}"); - if (tree.Success) { PrintFieldValuePair("Animation", $"{address:X}"); @@ -334,7 +331,6 @@ public readonly unsafe partial struct TimelineTree if (columns.Count > 0) { using var tbl = ImRaii.Table($"##{(nint)this.node}animTable{a}", columns.Count, Borders | SizingFixedFit | RowBg | NoHostExtendX); - if (tbl.Success) { foreach (var c in columns) diff --git a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs index 2ea8fd5d2..c8e2e2a55 100644 --- a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs +++ b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs @@ -159,65 +159,61 @@ internal unsafe class ElementSelector : IDisposable if (ch.Success) { using var gr = ImRaii.Group(); - if (gr.Success) + Gui.PrintFieldValuePair("Mouse Position", $"{mousePos.X}, {mousePos.Y}"); + ImGui.Spacing(); + ImGui.Text("RESULTS:\n"u8); + + var i = 0; + foreach (var a in addonResults) { - Gui.PrintFieldValuePair("Mouse Position", $"{mousePos.X}, {mousePos.Y}"); - ImGui.Spacing(); - ImGui.Text("RESULTS:\n"u8); + var name = a.Addon->NameString; + ImGui.Text($"[Addon] {name}"); - var i = 0; - foreach (var a in addonResults) + using var indent = ImRaii.PushIndent(15.0f); + foreach (var n in a.Nodes) { - var name = a.Addon->NameString; - ImGui.Text($"[Addon] {name}"); - ImGui.Indent(15); - foreach (var n in a.Nodes) + var nSelected = i++ == this.index; + + PrintNodeHeaderOnly(n.Node, nSelected, a.Addon); + + if (nSelected && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { - var nSelected = i++ == this.index; + this.Active = false; - PrintNodeHeaderOnly(n.Node, nSelected, a.Addon); + this.uiDebug2.SelectedAddonName = a.Addon->NameString; - if (nSelected && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + var ptrList = new List { (nint)n.Node }; + + var nextNode = n.Node->ParentNode; + while (nextNode != null) { - this.Active = false; - - this.uiDebug2.SelectedAddonName = a.Addon->NameString; - - var ptrList = new List { (nint)n.Node }; - - var nextNode = n.Node->ParentNode; - while (nextNode != null) - { - ptrList.Add((nint)nextNode); - nextNode = nextNode->ParentNode; - } - - SearchResults = [.. ptrList]; - Countdown = 100; - Scrolled = false; + ptrList.Add((nint)nextNode); + nextNode = nextNode->ParentNode; } - if (nSelected) - { - n.NodeBounds.DrawFilled(new(1, 1, 0.2f, 1)); - } + SearchResults = [.. ptrList]; + Countdown = 100; + Scrolled = false; } - ImGui.Indent(-15); + if (nSelected) + { + n.NodeBounds.DrawFilled(new(1, 1, 0.2f, 1)); + } + } + } + + if (i != 0) + { + this.index -= (int)ImGui.GetIO().MouseWheel; + while (this.index < 0) + { + this.index += i; } - if (i != 0) + while (this.index >= i) { - this.index -= (int)ImGui.GetIO().MouseWheel; - while (this.index < 0) - { - this.index += i; - } - - while (this.index >= i) - { - this.index -= i; - } + this.index -= i; } } } diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs b/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs index 4684caa60..69fbc17fb 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs @@ -39,7 +39,7 @@ internal class AddonPopoutWindow : Window, IDisposable /// public override void Draw() { - using var ch = ImRaii.Child($"{this.WindowName}child", new(-1, -1), true); + using var ch = ImRaii.Child($"{this.WindowName}child", Vector2.Zero, true); if (ch.Success) { this.addonTree.Draw(); diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs index da4b95256..60b51e5fb 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs @@ -38,7 +38,7 @@ internal unsafe class NodePopoutWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Once; this.SizeCondition = ImGuiCond.Once; this.Size = new(700, 200); - this.SizeConstraints = new() { MinimumSize = new(100, 100) }; + this.SizeConstraints = new() { MinimumSize = new Vector2(100, 100) }; } private AddonTree AddonTree => this.resNodeTree.AddonTree; @@ -50,7 +50,7 @@ internal unsafe class NodePopoutWindow : Window, IDisposable { if (this.Node != null && this.AddonTree.ContainsNode(this.Node)) { - using var ch = ImRaii.Child($"{(nint)this.Node:X}popoutChild", new(-1, -1), true); + using var ch = ImRaii.Child($"{(nint)this.Node:X}popoutChild", Vector2.Zero, true); if (ch.Success) { ResNodeTree.GetOrCreate(this.Node, this.AddonTree).Print(null, this.firstDraw); diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs index da5f30e68..1c293ff43 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs @@ -104,12 +104,8 @@ internal static class Gui var index = (int)Math.Floor(prog * tooltips.Length); - using var tt = ImRaii.Tooltip(); - - if (tt.Success) - { - ImGui.Text(tooltips[index]); - } + using var tooltip = ImRaii.Tooltip(); + ImGui.Text(tooltips[index]); return true; } @@ -123,13 +119,14 @@ internal static class Gui { if ((mask & 0b10) > 0) { - ImGui.Dummy(new(padding * ImGui.GetIO().FontGlobalScale)); + ImGuiHelpers.ScaledDummy(padding); } ImGui.Separator(); + if ((mask & 0b01) > 0) { - ImGui.Dummy(new(padding * ImGui.GetIO().FontGlobalScale)); + ImGuiHelpers.ScaledDummy(padding); } } } diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs index 832c7f357..ea471476e 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs @@ -61,10 +61,11 @@ public unsafe struct NodeBounds return; } + var backgroundDrawList = ImGui.GetBackgroundDrawList(); if (this.Points.Count == 1) { - ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12, thickness); - ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], thickness, RgbaVector4ToUint(col), 12, thickness + 1); + backgroundDrawList.AddCircle(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12, thickness); + backgroundDrawList.AddCircle(this.Points[0], thickness, RgbaVector4ToUint(col), 12, thickness + 1); } else { @@ -74,8 +75,7 @@ public unsafe struct NodeBounds path.Add(p); } - ImGui.GetBackgroundDrawList() - .AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness); + backgroundDrawList.AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness); path.Dispose(); } @@ -93,11 +93,11 @@ public unsafe struct NodeBounds return; } + var backgroundDrawList = ImGui.GetBackgroundDrawList(); if (this.Points.Count == 1) { - ImGui.GetBackgroundDrawList() - .AddCircleFilled(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12); - ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, RgbaVector4ToUint(col), 12, thickness); + backgroundDrawList.AddCircleFilled(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12); + backgroundDrawList.AddCircle(this.Points[0], 10, RgbaVector4ToUint(col), 12, thickness); } else { @@ -107,10 +107,8 @@ public unsafe struct NodeBounds path.Add(p); } - ImGui.GetBackgroundDrawList() - .AddConvexPolyFilled(ref path[0], path.Length, RgbaVector4ToUint(col with { W = col.W / 2 })); - ImGui.GetBackgroundDrawList() - .AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness); + backgroundDrawList.AddConvexPolyFilled(ref path[0], path.Length, RgbaVector4ToUint(col with { W = col.W / 2 })); + backgroundDrawList.AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness); path.Dispose(); } From 27414d33dd7e0ef8958c9f47341d573497490a5c Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Mon, 5 Jan 2026 03:40:08 +0100 Subject: [PATCH 404/477] Update ClientStructs (#2569) 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 e1e99cf46..b6f886afc 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e1e99cf469f87b0f3e8664ee3e3650dff86df2d6 +Subproject commit b6f886afc2b1a54d8fd76c37a260a05f214a559e From d0caf98eb3d0333ee7fb3226fefca77fecc052f0 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 4 Jan 2026 21:40:31 -0800 Subject: [PATCH 405/477] Add Agent Lifecycle --- Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs | 37 ++ .../AgentArgTypes/AgentClassJobChangeArgs.cs | 22 + .../Agent/AgentArgTypes/AgentGameEventArgs.cs | 22 + .../AgentArgTypes/AgentLevelChangeArgs.cs | 27 ++ .../AgentArgTypes/AgentReceiveEventArgs.cs | 37 ++ Dalamud/Game/Agent/AgentArgsType.cs | 32 ++ Dalamud/Game/Agent/AgentEvent.cs | 87 ++++ Dalamud/Game/Agent/AgentLifecycle.cs | 315 ++++++++++++++ .../Game/Agent/AgentLifecycleEventListener.cs | 38 ++ Dalamud/Game/Agent/AgentVirtualTable.cs | 393 ++++++++++++++++++ Dalamud/Plugin/Services/IAgentLifecycle.cs | 88 ++++ 11 files changed, 1098 insertions(+) create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgsType.cs create mode 100644 Dalamud/Game/Agent/AgentEvent.cs create mode 100644 Dalamud/Game/Agent/AgentLifecycle.cs create mode 100644 Dalamud/Game/Agent/AgentLifecycleEventListener.cs create mode 100644 Dalamud/Game/Agent/AgentVirtualTable.cs create mode 100644 Dalamud/Plugin/Services/IAgentLifecycle.cs diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs new file mode 100644 index 000000000..b4a904dde --- /dev/null +++ b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Game.Agent.AgentArgTypes; + +/// +/// Base class for AgentLifecycle AgentArgTypes. +/// +public unsafe class AgentArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AgentArgs() + { + } + + /// + /// Gets the pointer to the Agents AgentInterface*. + /// + public nint Agent { get; internal set; } + + /// + /// Gets the agent id. + /// + public uint AgentId { get; internal set; } + + /// + /// Gets the type of these args. + /// + public virtual AgentArgsType Type => AgentArgsType.Generic; + + /// + /// Gets the typed pointer to the Agents AgentInterface*. + /// + /// AgentInterface. + /// Typed pointer to contained Agents AgentInterface. + public T* GetAgentPointer() where T : unmanaged + => (T*)this.Agent; +} diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs new file mode 100644 index 000000000..351760963 --- /dev/null +++ b/Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Game.Agent.AgentArgTypes; + +/// +/// Agent argument data for game events. +/// +public class AgentClassJobChangeArgs : AgentArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AgentClassJobChangeArgs() + { + } + + /// + public override AgentArgsType Type => AgentArgsType.ClassJobChange; + + /// + /// Gets or sets a value indicating what the new ClassJob is. + /// + public byte ClassJobId { get; set; } +} diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs new file mode 100644 index 000000000..3da601707 --- /dev/null +++ b/Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Game.Agent.AgentArgTypes; + +/// +/// Agent argument data for game events. +/// +public class AgentGameEventArgs : AgentArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AgentGameEventArgs() + { + } + + /// + public override AgentArgsType Type => AgentArgsType.GameEvent; + + /// + /// Gets or sets a value representing which gameEvent was triggered. + /// + public int GameEvent { get; set; } +} diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs new file mode 100644 index 000000000..a74371ebb --- /dev/null +++ b/Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.Agent.AgentArgTypes; + +/// +/// Agent argument data for game events. +/// +public class AgentLevelChangeArgs : AgentArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AgentLevelChangeArgs() + { + } + + /// + public override AgentArgsType Type => AgentArgsType.LevelChange; + + /// + /// Gets or sets a value indicating which ClassJob was switched to. + /// + public byte ClassJobId { get; set; } + + /// + /// Gets or sets a value indicating what the new level is. + /// + public ushort Level { get; set; } +} diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs new file mode 100644 index 000000000..01e1f25f6 --- /dev/null +++ b/Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Game.Agent.AgentArgTypes; + +/// +/// Agent argument data for ReceiveEvent events. +/// +public class AgentReceiveEventArgs : AgentArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AgentReceiveEventArgs() + { + } + + /// + public override AgentArgsType Type => AgentArgsType.ReceiveEvent; + + /// + /// Gets or sets the AtkValue return value for this event message. + /// + public nint ReturnValue { get; set; } + + /// + /// Gets or sets the AtkValue array for this event message. + /// + public nint AtkValues { get; set; } + + /// + /// Gets or sets the AtkValue count for this event message. + /// + public uint ValueCount { get; set; } + + /// + /// Gets or sets the event kind for this event message. + /// + public ulong EventKind { get; set; } +} diff --git a/Dalamud/Game/Agent/AgentArgsType.cs b/Dalamud/Game/Agent/AgentArgsType.cs new file mode 100644 index 000000000..0c96c0135 --- /dev/null +++ b/Dalamud/Game/Agent/AgentArgsType.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Game.Agent; + +/// +/// Enumeration for available AgentLifecycle arg data. +/// +public enum AgentArgsType +{ + /// + /// Generic arg type that contains no meaningful data. + /// + Generic, + + /// + /// Contains argument data for ReceiveEvent. + /// + ReceiveEvent, + + /// + /// Contains argument data for GameEvent. + /// + GameEvent, + + /// + /// Contains argument data for LevelChange. + /// + LevelChange, + + /// + /// Contains argument data for ClassJobChange. + /// + ClassJobChange, +} diff --git a/Dalamud/Game/Agent/AgentEvent.cs b/Dalamud/Game/Agent/AgentEvent.cs new file mode 100644 index 000000000..2a3002daa --- /dev/null +++ b/Dalamud/Game/Agent/AgentEvent.cs @@ -0,0 +1,87 @@ +namespace Dalamud.Game.Agent; + +/// +/// Enumeration for available AgentLifecycle events. +/// +public enum AgentEvent +{ + /// + /// An event that is fired before the agent processes its Receive Event Function. + /// + PreReceiveEvent, + + /// + /// An event that is fired after the agent has processed its Receive Event Function. + /// + PostReceiveEvent, + + /// + /// An event that is fired before the agent processes its Filtered Receive Event Function. + /// + PreReceiveFilteredEvent, + + /// + /// An event that is fired after the agent has processed its Filtered Receive Event Function. + /// + PostReceiveFilteredEvent, + + /// + /// An event that is fired before the agent processes its Show Function. + /// + PreShow, + + /// + /// An event that is fired after the agent has processed its Show Function. + /// + PostShow, + + /// + /// An event that is fired before the agent processes its Hide Function. + /// + PreHide, + + /// + /// An event that is fired after the agent has processed its Hide Function. + /// + PostHide, + + /// + /// An event that is fired before the agent processes its Update Function. + /// + PreUpdate, + + /// + /// An event that is fired after the agent has processed its Update Function. + /// + PostUpdate, + + /// + /// An event that is fired before the agent processes its Game Event Function. + /// + PreGameEvent, + + /// + /// An event that is fired after the agent has processed its Game Event Function. + /// + PostGameEvent, + + /// + /// An event that is fired before the agent processes its Game Event Function. + /// + PreLevelChange, + + /// + /// An event that is fired after the agent has processed its Level Change Function. + /// + PostLevelChange, + + /// + /// An event that is fired before the agent processes its ClassJob Change Function. + /// + PreClassJobChange, + + /// + /// An event that is fired after the agent has processed its ClassJob Change Function. + /// + PostClassJobChange, +} diff --git a/Dalamud/Game/Agent/AgentLifecycle.cs b/Dalamud/Game/Agent/AgentLifecycle.cs new file mode 100644 index 000000000..1306a92c1 --- /dev/null +++ b/Dalamud/Game/Agent/AgentLifecycle.cs @@ -0,0 +1,315 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +using Dalamud.Game.Agent.AgentArgTypes; +using Dalamud.Hooking; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.Interop; + +namespace Dalamud.Game.Agent; + +/// +/// This class provides events for in-game agent lifecycles. +/// +[ServiceManager.EarlyLoadedService] +internal unsafe class AgentLifecycle : IInternalDisposableService +{ + /// + /// Gets a list of all allocated agent virtual tables. + /// + public static readonly List AllocatedTables = []; + + private static readonly ModuleLog Log = new("AgentLifecycle"); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + private Hook? onInitializeAgentsHook; + private bool isInvokingListeners; + + [ServiceManager.ServiceConstructor] + private AgentLifecycle() + { + var agentModuleInstance = AgentModule.Instance(); + + // Hook is only used to determine appropriate timing for replacing Agent Virtual Tables + // If the agent module is already initialized, then we can replace the tables safely. + if (agentModuleInstance is null) + { + this.onInitializeAgentsHook = Hook.FromAddress((nint)AgentModule.MemberFunctionPointers.Ctor, this.OnAgentModuleInitialize); + this.onInitializeAgentsHook.Enable(); + } + else + { + // For safety because this might be injected async, we will make sure we are on the main thread first. + this.framework.RunOnFrameworkThread(() => this.ReplaceVirtualTables(agentModuleInstance)); + } + } + + /// + /// Gets a list of all AgentLifecycle Event Listeners. + ///
+ /// Mapping is: EventType -> ListenerList + internal Dictionary>> EventListeners { get; } = []; + + /// + void IInternalDisposableService.DisposeService() + { + this.onInitializeAgentsHook?.Dispose(); + this.onInitializeAgentsHook = null; + + AllocatedTables.ForEach(entry => entry.Dispose()); + AllocatedTables.Clear(); + } + + /// + /// Register a listener for the target event and agent. + /// + /// The listener to register. + internal void RegisterListener(AgentLifecycleEventListener listener) + { + this.framework.RunOnTick(() => + { + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + if (!this.EventListeners.TryAdd(listener.EventType, [])) + return; + } + + // Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId)) + { + if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, [])) + return; + } + + this.EventListeners[listener.EventType][listener.AgentId].Add(listener); + }, + delayTicks: this.isInvokingListeners ? 1 : 0); + } + + /// + /// Unregisters the listener from events. + /// + /// The listener to unregister. + internal void UnregisterListener(AgentLifecycleEventListener listener) + { + this.framework.RunOnTick(() => + { + if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners)) + { + if (agentListeners.TryGetValue(listener.AgentId, out var agentListener)) + { + agentListener.Remove(listener); + } + } + }, + delayTicks: this.isInvokingListeners ? 1 : 0); + } + + /// + /// Invoke listeners for the specified event type. + /// + /// Event Type. + /// AgentARgs. + /// What to blame on errors. + internal void InvokeListenersSafely(AgentEvent eventType, AgentArgs args, [CallerMemberName] string blame = "") + { + this.isInvokingListeners = true; + + // Early return if we don't have any listeners of this type + if (!this.EventListeners.TryGetValue(eventType, out var agentListeners)) return; + + // Handle listeners for this event type that don't care which agent is triggering it + if (agentListeners.TryGetValue(uint.MaxValue, out var globalListeners)) + { + foreach (var listener in globalListeners) + { + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global agent event listener."); + } + } + } + + // Handle listeners that are listening for this agent and event type specifically + if (agentListeners.TryGetValue(args.AgentId, out var agentListener)) + { + foreach (var listener in agentListener) + { + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific agent {(AgentId)args.AgentId}."); + } + } + } + + this.isInvokingListeners = false; + } + + /// + /// Resolves a virtual table address to the original virtual table address. + /// + /// The modified address to resolve. + /// The original address. + internal AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress) + { + var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); + if (matchedTable == null) return null; + + return matchedTable.OriginalVirtualTable; + } + + private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule) + { + this.onInitializeAgentsHook!.Original(thisPtr, uiModule); + + try + { + this.ReplaceVirtualTables(thisPtr); + + // We don't need this hook anymore, it did its job! + this.onInitializeAgentsHook!.Dispose(); + this.onInitializeAgentsHook = null; + } + catch (Exception e) + { + Log.Error(e, "Exception in AgentLifecycle during AgentModule Ctor."); + } + } + + private void ReplaceVirtualTables(AgentModule* agentModule) + { + foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length)) + { + try + { + var agentPointer = agentModule->Agents.GetPointer((int)index); + + if (agentPointer is null) + { + Log.Warning("Null Agent Found?"); + continue; + } + + // AgentVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, index, this)); + } + catch (Exception e) + { + Log.Error(e, "Exception in AgentLifecycle during ReplaceVirtualTables."); + } + } + } +} + +/// +/// Plugin-scoped version of a AgentLifecycle service. +/// +[PluginInterface] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLifecycle +{ + [ServiceManager.ServiceDependency] + private readonly AgentLifecycle agentLifecycleService = Service.Get(); + + private readonly List eventListeners = []; + + /// + void IInternalDisposableService.DisposeService() + { + foreach (var listener in this.eventListeners) + { + this.agentLifecycleService.UnregisterListener(listener); + } + } + + /// + public void RegisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate handler) + { + foreach (var agentId in agentIds) + { + this.RegisterListener(eventType, agentId, handler); + } + } + + /// + public void RegisterListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate handler) + { + var listener = new AgentLifecycleEventListener(eventType, agentId, handler); + this.eventListeners.Add(listener); + this.agentLifecycleService.RegisterListener(listener); + } + + /// + public void RegisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate handler) + { + this.RegisterListener(eventType, uint.MaxValue, handler); + } + + /// + public void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate? handler = null) + { + foreach (var agentId in agentIds) + { + this.UnregisterListener(eventType, agentId, handler); + } + } + + /// + public void UnregisterListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate? handler = null) + { + this.eventListeners.RemoveAll(entry => + { + if (entry.EventType != eventType) return false; + if (entry.AgentId != agentId) return false; + if (handler is not null && entry.FunctionDelegate != handler) return false; + + this.agentLifecycleService.UnregisterListener(entry); + return true; + }); + } + + /// + public void UnregisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate? handler = null) + { + this.UnregisterListener(eventType, uint.MaxValue, handler); + } + + /// + public void UnregisterListener(params IAgentLifecycle.AgentEventDelegate[] handlers) + { + foreach (var handler in handlers) + { + this.eventListeners.RemoveAll(entry => + { + if (entry.FunctionDelegate != handler) return false; + + this.agentLifecycleService.UnregisterListener(entry); + return true; + }); + } + } + + /// + public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) + => (nint)this.agentLifecycleService.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress); +} diff --git a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs new file mode 100644 index 000000000..3521d2c13 --- /dev/null +++ b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs @@ -0,0 +1,38 @@ +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Agent; + +/// +/// This class is a helper for tracking and invoking listener delegates. +/// +public class AgentLifecycleEventListener +{ + /// + /// Initializes a new instance of the class. + /// + /// Event type to listen for. + /// Agent id to listen for. + /// Delegate to invoke. + internal AgentLifecycleEventListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate functionDelegate) + { + this.EventType = eventType; + this.AgentId = agentId; + this.FunctionDelegate = functionDelegate; + } + + /// + /// Gets the agentId of the agent this listener is looking for. + /// uint.MaxValue if it wants to be called for any agent. + /// + public uint AgentId { get; init; } + + /// + /// Gets the event type this listener is looking for. + /// + public AgentEvent EventType { get; init; } + + /// + /// Gets the delegate this listener invokes. + /// + public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; } +} diff --git a/Dalamud/Game/Agent/AgentVirtualTable.cs b/Dalamud/Game/Agent/AgentVirtualTable.cs new file mode 100644 index 000000000..3c23616e8 --- /dev/null +++ b/Dalamud/Game/Agent/AgentVirtualTable.cs @@ -0,0 +1,393 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +using Dalamud.Game.Agent.AgentArgTypes; +using Dalamud.Logging.Internal; + +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Agent; + +/// +/// Represents a class that holds references to an agents original and modified virtual table entries. +/// +internal unsafe class AgentVirtualTable : IDisposable +{ + // This need to be at minimum the largest virtual table size of all agents + // Copying extra entries is not problematic, and is considered safe. + private const int VirtualTableEntryCount = 60; + + private const bool EnableLogging = true; + + private static readonly ModuleLog Log = new("AgentVT"); + + private readonly AgentLifecycle lifecycleService; + + private readonly uint agentId; + + // Each agent gets its own set of args that are used to mutate the original call when used in pre-calls + private readonly AgentReceiveEventArgs receiveEventArgs = new(); + private readonly AgentReceiveEventArgs filteredReceiveEventArgs = new(); + private readonly AgentArgs showArgs = new(); + private readonly AgentArgs hideArgs = new(); + private readonly AgentArgs updateArgs = new(); + private readonly AgentGameEventArgs gameEventArgs = new(); + private readonly AgentLevelChangeArgs levelChangeArgs = new(); + private readonly AgentClassJobChangeArgs classJobChangeArgs = new(); + + private readonly AgentInterface* agentInterface; + + // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table, + // the CLR needs to know they are in use, or it will invalidate them causing random crashing. + private readonly AgentInterface.Delegates.ReceiveEvent receiveEventFunction; + private readonly AgentInterface.Delegates.ReceiveEvent2 filteredReceiveEventFunction; + private readonly AgentInterface.Delegates.Show showFunction; + private readonly AgentInterface.Delegates.Hide hideFunction; + private readonly AgentInterface.Delegates.Update updateFunction; + private readonly AgentInterface.Delegates.OnGameEvent gameEventFunction; + private readonly AgentInterface.Delegates.OnLevelChange levelChangeFunction; + private readonly AgentInterface.Delegates.OnClassJobChange classJobChangeFunction; + + /// + /// Initializes a new instance of the class. + /// + /// AgentInterface* for the agent to replace the table of. + /// Agent ID. + /// Reference to AgentLifecycle service to callback and invoke listeners. + internal AgentVirtualTable(AgentInterface* agent, uint agentId, AgentLifecycle lifecycleService) + { + Log.Debug($"Initializing AgentVirtualTable for {(AgentId)agentId}, Address: {(nint)agent:X}"); + + this.agentInterface = agent; + this.agentId = agentId; + this.lifecycleService = lifecycleService; + + // Save original virtual table + this.OriginalVirtualTable = agent->VirtualTable; + + // Create copy of original table + // Note this will copy any derived/overriden functions that this specific agent has. + // Note: currently there are 16 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game + this.ModifiedVirtualTable = (AgentInterface.AgentInterfaceVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8); + NativeMemory.Copy(agent->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount); + + // Overwrite the agents existing virtual table with our own + agent->VirtualTable = this.ModifiedVirtualTable; + + // Pin each of our listener functions + this.receiveEventFunction = this.OnAgentReceiveEvent; + this.filteredReceiveEventFunction = this.OnAgentFilteredReceiveEvent; + this.showFunction = this.OnAgentShow; + this.hideFunction = this.OnAgentHide; + this.updateFunction = this.OnAgentUpdate; + this.gameEventFunction = this.OnAgentGameEvent; + this.levelChangeFunction = this.OnAgentLevelChange; + this.classJobChangeFunction = this.OnClassJobChange; + + // Overwrite specific virtual table entries + this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction); + this.ModifiedVirtualTable->ReceiveEvent2 = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.filteredReceiveEventFunction); + this.ModifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); + this.ModifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); + this.ModifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); + this.ModifiedVirtualTable->OnGameEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.gameEventFunction); + this.ModifiedVirtualTable->OnLevelChange = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.levelChangeFunction); + this.ModifiedVirtualTable->OnClassJobChange = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.classJobChangeFunction); + } + + /// + /// Gets the original virtual table address for this agent. + /// + internal AgentInterface.AgentInterfaceVirtualTable* OriginalVirtualTable { get; private set; } + + /// + /// Gets the modified virtual address for this agent. + /// + internal AgentInterface.AgentInterfaceVirtualTable* ModifiedVirtualTable { get; private set; } + + /// + public void Dispose() + { + // Ensure restoration is done atomically. + Interlocked.Exchange(ref *(nint*)&this.agentInterface->VirtualTable, (nint)this.OriginalVirtualTable); + IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount); + } + + private AtkValue* OnAgentReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) + { + AtkValue* result = null; + + try + { + this.LogEvent(EnableLogging); + + this.receiveEventArgs.Agent = (nint)thisPtr; + this.receiveEventArgs.AgentId = this.agentId; + this.receiveEventArgs.ReturnValue = (nint)returnValue; + this.receiveEventArgs.AtkValues = (nint)values; + this.receiveEventArgs.ValueCount = valueCount; + this.receiveEventArgs.EventKind = eventKind; + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEvent, this.receiveEventArgs); + + returnValue = (AtkValue*)this.receiveEventArgs.ReturnValue; + values = (AtkValue*)this.receiveEventArgs.AtkValues; + valueCount = this.receiveEventArgs.ValueCount; + eventKind = this.receiveEventArgs.EventKind; + + try + { + result = this.OriginalVirtualTable->ReceiveEvent(thisPtr, returnValue, values, valueCount, eventKind); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Agent ReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEvent, this.receiveEventArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEvent."); + } + + return result; + } + + private AtkValue* OnAgentFilteredReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) + { + AtkValue* result = null; + + try + { + this.LogEvent(EnableLogging); + + this.filteredReceiveEventArgs.Agent = (nint)thisPtr; + this.filteredReceiveEventArgs.AgentId = this.agentId; + this.filteredReceiveEventArgs.ReturnValue = (nint)returnValue; + this.filteredReceiveEventArgs.AtkValues = (nint)values; + this.filteredReceiveEventArgs.ValueCount = valueCount; + this.filteredReceiveEventArgs.EventKind = eventKind; + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveFilteredEvent, this.filteredReceiveEventArgs); + + returnValue = (AtkValue*)this.filteredReceiveEventArgs.ReturnValue; + values = (AtkValue*)this.filteredReceiveEventArgs.AtkValues; + valueCount = this.filteredReceiveEventArgs.ValueCount; + eventKind = this.filteredReceiveEventArgs.EventKind; + + try + { + result = this.OriginalVirtualTable->ReceiveEvent2(thisPtr, returnValue, values, valueCount, eventKind); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Agent FilteredReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveFilteredEvent, this.filteredReceiveEventArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentFilteredReceiveEvent."); + } + + return result; + } + + private void OnAgentShow(AgentInterface* thisPtr) + { + try + { + this.LogEvent(EnableLogging); + + this.showArgs.Agent = (nint)thisPtr; + this.showArgs.AgentId = this.agentId; + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreShow, this.showArgs); + + try + { + this.OriginalVirtualTable->Show(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Show. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostShow, this.showArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentShow."); + } + } + + private void OnAgentHide(AgentInterface* thisPtr) + { + try + { + this.LogEvent(EnableLogging); + + this.hideArgs.Agent = (nint)thisPtr; + this.hideArgs.AgentId = this.agentId; + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreHide, this.hideArgs); + + try + { + this.OriginalVirtualTable->Hide(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Hide. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostHide, this.hideArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentHide."); + } + } + + private void OnAgentUpdate(AgentInterface* thisPtr, uint frameCount) + { + try + { + this.LogEvent(EnableLogging); + + this.updateArgs.Agent = (nint)thisPtr; + this.updateArgs.AgentId = this.agentId; + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreUpdate, this.updateArgs); + + try + { + this.OriginalVirtualTable->Update(thisPtr, frameCount); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Update. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostUpdate, this.updateArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentUpdate."); + } + } + + private void OnAgentGameEvent(AgentInterface* thisPtr, AgentInterface.GameEvent gameEvent) + { + try + { + this.LogEvent(EnableLogging); + + this.gameEventArgs.Agent = (nint)thisPtr; + this.gameEventArgs.AgentId = this.agentId; + this.gameEventArgs.GameEvent = (int)gameEvent; + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreGameEvent, this.gameEventArgs); + + gameEvent = (AgentInterface.GameEvent)this.gameEventArgs.GameEvent; + + try + { + this.OriginalVirtualTable->OnGameEvent(thisPtr, gameEvent); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnGameEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostGameEvent, this.gameEventArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentGameEvent."); + } + } + + private void OnAgentLevelChange(AgentInterface* thisPtr, byte classJobId, ushort level) + { + try + { + this.LogEvent(EnableLogging); + + this.levelChangeArgs.Agent = (nint)thisPtr; + this.levelChangeArgs.AgentId = this.agentId; + this.levelChangeArgs.ClassJobId = classJobId; + this.levelChangeArgs.Level = level; + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreLevelChange, this.levelChangeArgs); + + classJobId = this.levelChangeArgs.ClassJobId; + level = this.levelChangeArgs.Level; + + try + { + this.OriginalVirtualTable->OnLevelChange(thisPtr, classJobId, level); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnLevelChange. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostLevelChange, this.levelChangeArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentLevelChange."); + } + } + + private void OnClassJobChange(AgentInterface* thisPtr, byte classJobId) + { + try + { + this.LogEvent(EnableLogging); + + this.classJobChangeArgs.Agent = (nint)thisPtr; + this.classJobChangeArgs.AgentId = this.agentId; + this.classJobChangeArgs.ClassJobId = classJobId; + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreClassJobChange, this.classJobChangeArgs); + + classJobId = this.classJobChangeArgs.ClassJobId; + + try + { + this.OriginalVirtualTable->OnClassJobChange(thisPtr, classJobId); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnClassJobChange. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostClassJobChange, this.classJobChangeArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnClassJobChange."); + } + } + + [Conditional("DEBUG")] + private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") + { + if (loggingEnabled) + { + // Manually disable the really spammy log events, you can comment this out if you need to debug them. + if (caller is "OnAgentUpdate" || (AgentId)this.agentId is AgentId.PadMouseMode) + return; + + Log.Debug($"[{caller}]: {(AgentId)this.agentId}"); + } + } +} diff --git a/Dalamud/Plugin/Services/IAgentLifecycle.cs b/Dalamud/Plugin/Services/IAgentLifecycle.cs new file mode 100644 index 000000000..a1ed26125 --- /dev/null +++ b/Dalamud/Plugin/Services/IAgentLifecycle.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Dalamud.Game.Agent; +using Dalamud.Game.Agent.AgentArgTypes; + +namespace Dalamud.Plugin.Services; + +/// +/// This class provides events for in-game agent lifecycles. +/// +public interface IAgentLifecycle : IDalamudService +{ + /// + /// Delegate for receiving agent lifecycle event messages. + /// + /// The event type that triggered the message. + /// Information about what agent triggered the message. + public delegate void AgentEventDelegate(AgentEvent type, AgentArgs args); + + /// + /// Register a listener that will trigger on the specified event and any of the specified agent. + /// + /// Event type to trigger on. + /// Agent IDs that will trigger the handler to be invoked. + /// The handler to invoke. + void RegisterListener(AgentEvent eventType, IEnumerable agentIds, AgentEventDelegate handler); + + /// + /// Register a listener that will trigger on the specified event only for the specified agent. + /// + /// Event type to trigger on. + /// The agent ID that will trigger the handler to be invoked. + /// The handler to invoke. + void RegisterListener(AgentEvent eventType, uint agentId, AgentEventDelegate handler); + + /// + /// Register a listener that will trigger on the specified event for any agent. + /// + /// Event type to trigger on. + /// The handler to invoke. + void RegisterListener(AgentEvent eventType, AgentEventDelegate handler); + + /// + /// Unregister listener from specified event type and specified agent IDs. + /// + /// + /// If a specific handler is not provided, all handlers for the event type and agent IDs will be unregistered. + /// + /// Event type to deregister. + /// Agent IDs to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, [Optional] AgentEventDelegate handler); + + /// + /// Unregister all listeners for the specified event type and agent ID. + /// + /// + /// If a specific handler is not provided, all handlers for the event type and agents will be unregistered. + /// + /// Event type to deregister. + /// Agent id to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AgentEvent eventType, uint agentId, [Optional] AgentEventDelegate handler); + + /// + /// Unregister an event type handler.
This will only remove a handler that is added via . + ///
+ /// + /// If a specific handler is not provided, all handlers for the event type and agents will be unregistered. + /// + /// Event type to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AgentEvent eventType, [Optional] AgentEventDelegate handler); + + /// + /// Unregister all events that use the specified handlers. + /// + /// Handlers to remove. + void UnregisterListener(params AgentEventDelegate[] handlers); + + /// + /// Resolves an agents virtual table address back to the original unmodified table address. + /// + /// The address of a modified agents virtual table. + /// The address of the agents original virtual table. + nint GetOriginalVirtualTable(nint virtualTableAddress); +} From e94ded628adcfcbd12f207eb6443b00482e02e6e Mon Sep 17 00:00:00 2001 From: Loskh <1020612624@qq.com> Date: Mon, 5 Jan 2026 22:42:04 +0800 Subject: [PATCH 406/477] fix: respect system dark mode setting --- .../Interface/Internal/InterfaceManager.cs | 43 ++++++++++++++++++- .../Windows/Settings/Tabs/SettingsTabLook.cs | 9 +++- Dalamud/Memory/MemoryHelper.cs | 25 +++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 96fcb7dfd..72a9eb0f2 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -32,6 +32,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing.Persistence; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Memory; using Dalamud.Plugin.Services; using Dalamud.Utility; using Dalamud.Utility.Timing; @@ -500,6 +501,34 @@ internal partial class InterfaceManager : IInternalDisposableService ImGuiHelpers.ClearStacksOnContext(); } + /// + /// Applies immersive dark mode to the game window based on the current system theme setting. + /// + internal void SetImmersiveModeFromSystemTheme() + { + bool useDark = this.IsSystemInDarkMode(); + this.SetImmersiveMode(useDark); + } + + /// + /// Checks whether the system use dark mode. + /// + /// Returns true if dark mode is preferred. + internal bool IsSystemInDarkMode() + { + try + { + using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey( + @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + var value = key?.GetValue("AppsUseLightTheme") as int?; + return value != 1; + } + catch + { + return false; + } + } + /// /// Toggle Windows 11 immersive mode on the game window. /// @@ -744,6 +773,18 @@ internal partial class InterfaceManager : IInternalDisposableService private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) { + if (args.Message == WM.WM_SETTINGCHANGE) + { + if (this.dalamudConfiguration.WindowIsImmersive) + { + if (MemoryHelper.EqualsZeroTerminatedWideString("ImmersiveColorSet", args.LParam) || + MemoryHelper.EqualsZeroTerminatedWideString("VisualStyleChanged", args.LParam)) + { + this.SetImmersiveModeFromSystemTheme(); + } + } + } + var r = this.backend?.ProcessWndProcW(args.Hwnd, args.Message, args.WParam, args.LParam); if (r is not null) args.SuppressWithValue(r.Value); @@ -858,7 +899,7 @@ internal partial class InterfaceManager : IInternalDisposableService { // Requires that game window to be there, which will be the case once game swap chain is initialized. if (Service.Get().WindowIsImmersive) - this.SetImmersiveMode(true); + this.SetImmersiveModeFromSystemTheme(); } catch (Exception ex) { diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 9b2c418b6..3bb16ca74 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -65,7 +65,14 @@ internal sealed class SettingsTabLook : SettingsTab { try { - Service.GetNullable()?.SetImmersiveMode(b); + if (b) + { + Service.GetNullable()?.SetImmersiveModeFromSystemTheme(); + } + else + { + Service.GetNullable()?.SetImmersiveMode(false); + } } catch (Exception ex) { diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs index 2eae1be6d..f0f4c991f 100644 --- a/Dalamud/Memory/MemoryHelper.cs +++ b/Dalamud/Memory/MemoryHelper.cs @@ -264,6 +264,31 @@ public static unsafe class MemoryHelper } } + /// + /// Compares a UTF-16 character span with a null-terminated UTF-16 string at . + /// + /// UTF-16 character span (e.g., from a string literal). + /// Address of null-terminated UTF-16 (wide) string, as used by Windows APIs. + /// if equal; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool EqualsZeroTerminatedWideString( + scoped ReadOnlySpan charSpan, + nint memoryAddress) + { + if (memoryAddress == 0) + return charSpan.Length == 0; + + char* p = (char*)memoryAddress; + + foreach (char c in charSpan) + { + if (*p++ != c) + return false; + } + + return *p == '\0'; + } + /// /// Read a UTF-8 encoded string from a specified memory address. /// From 9b9a66bdd27ef73dd80c50b7d647e7760b9435d1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 6 Jan 2026 14:19:21 +0100 Subject: [PATCH 407/477] Update AddonEventType --- Dalamud/Game/Addon/Events/AddonEventType.cs | 65 +++++++++++++++++---- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventType.cs b/Dalamud/Game/Addon/Events/AddonEventType.cs index 25beb13fc..9e062a9d0 100644 --- a/Dalamud/Game/Addon/Events/AddonEventType.cs +++ b/Dalamud/Game/Addon/Events/AddonEventType.cs @@ -61,6 +61,11 @@ public enum AddonEventType : byte ///
InputBaseInputReceived = 15, + /// + /// Fired at the very beginning of AtkInputManager.HandleInput on AtkStage.ViewportEventManager. Used in LovmMiniMap. + /// + RawInputData = 16, + /// /// Focus Start. /// @@ -107,7 +112,12 @@ public enum AddonEventType : byte SliderReleased = 30, /// - /// AtkComponentList RollOver. + /// AtkComponentList Button Press. + /// + ListButtonPress = 31, + + /// + /// AtkComponentList Roll Over. /// ListItemRollOver = 33, @@ -126,11 +136,31 @@ public enum AddonEventType : byte ///
ListItemDoubleClick = 36, + /// + /// AtkComponentList Highlight. + /// + ListItemHighlight = 37, + /// /// AtkComponentList Select. /// ListItemSelect = 38, + /// + /// AtkComponentList Pad Drag Drop Begin. + /// + ListItemPadDragDropBegin = 40, + + /// + /// AtkComponentList Pad Drag Drop End. + /// + ListItemPadDragDropEnd = 41, + + /// + /// AtkComponentList Pad Drag Drop Insert. + /// + ListItemPadDragDropInsert = 42, + /// /// AtkComponentDragDrop Begin. /// Sent on MouseDown over a draggable icon (will NOT send for a locked icon). @@ -142,12 +172,22 @@ public enum AddonEventType : byte /// DragDropEnd = 51, + /// + /// AtkComponentDragDrop Insert Attempt. + /// + DragDropInsertAttempt = 52, + /// /// AtkComponentDragDrop Insert. /// Sent when dropping an icon into a hotbar/inventory slot or similar. /// DragDropInsert = 53, + /// + /// AtkComponentDragDrop Can Accept Check. + /// + DragDropCanAcceptCheck = 54, + /// /// AtkComponentDragDrop Roll Over. /// @@ -165,23 +205,18 @@ public enum AddonEventType : byte DragDropDiscard = 57, /// - /// Drag Drop Unknown. + /// AtkComponentDragDrop Click. + /// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon. /// - [Obsolete("Use DragDropDiscard", true)] - DragDropUnk54 = 54, + DragDropClick = 58, /// /// AtkComponentDragDrop Cancel. /// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon. /// + [Obsolete("Renamed to DragDropClick")] DragDropCancel = 58, - /// - /// Drag Drop Unknown. - /// - [Obsolete("Use DragDropCancel", true)] - DragDropUnk55 = 55, - /// /// AtkComponentIconText Roll Over. /// @@ -217,6 +252,11 @@ public enum AddonEventType : byte ///
TimerEnd = 65, + /// + /// AtkTimer Start. + /// + TimerStart = 66, + /// /// AtkSimpleTween Progress. /// @@ -247,6 +287,11 @@ public enum AddonEventType : byte ///
WindowChangeScale = 72, + /// + /// AtkTimeline Active Label Changed. + /// + TimelineActiveLabelChanged = 75, + /// /// AtkTextNode Link Mouse Click. /// From c93f04f0e47da91d5425f04f8fe44a804cffcc7a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 6 Jan 2026 17:36:55 +0100 Subject: [PATCH 408/477] Code cleanup (#2439) * Use new Lock objects * Fix CA1513: Use ObjectDisposedException.ThrowIf * Fix CA1860: Avoid using 'Enumerable.Any()' extension method * Fix IDE0028: Use collection initializers or expressions * Fix CA2263: Prefer generic overload when type is known * Fix CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons * Fix IDE0270: Null check can be simplified * Fix IDE0280: Use 'nameof' * Fix IDE0009: Add '.this' * Fix IDE0007: Use 'var' instead of explicit type * Fix IDE0062: Make local function static * Fix CA1859: Use concrete types when possible for improved performance * Fix IDE0066: Use switch expression Only applied to where it doesn't look horrendous. * Use is over switch * Fix CA1847: Use String.Contains(char) instead of String.Contains(string) with single characters * Fix SYSLIB1045: Use 'GeneratedRegexAttribute' to generate the regular expression implementation at compile-time. * Fix CA1866: Use 'string.EndsWith(char)' instead of 'string.EndsWith(string)' when you have a string with a single char * Fix IDE0057: Substring can be simplified * Fix IDE0059: Remove unnecessary value assignment * Fix CA1510: Use ArgumentNullException throw helper * Fix IDE0300: Use collection expression for array * Fix IDE0250: Struct can be made 'readonly' * Fix IDE0018: Inline variable declaration * Fix CA1850: Prefer static HashData method over ComputeHash * Fi CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' * Update ModuleLog instantiations * Organize usings --- .../Internal/DalamudConfiguration.cs | 20 +- .../Internal/DevPluginSettings.cs | 2 +- Dalamud/Console/ConsoleManager.cs | 13 +- Dalamud/Console/ConsoleManagerPluginScoped.cs | 4 +- Dalamud/Dalamud.cs | 2 + Dalamud/Data/DataManager.cs | 2 + Dalamud/Data/RsvResolver.cs | 4 +- Dalamud/EntryPoint.cs | 5 +- Dalamud/Game/Addon/Events/AddonEventEntry.cs | 1 - .../Game/Addon/Events/AddonEventManager.cs | 2 +- .../Addon/Events/PluginEventController.cs | 5 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/BaseAddressResolver.cs | 2 +- Dalamud/Game/ChatHandlers.cs | 3 +- .../ClientState/Aetherytes/AetheryteList.cs | 1 + Dalamud/Game/ClientState/ClientState.cs | 2 +- .../Game/ClientState/GamePad/GamepadState.cs | 2 + .../Game/ClientState/JobGauge/JobGauges.cs | 2 +- .../ClientState/JobGauge/Types/BRDGauge.cs | 7 +- .../ClientState/JobGauge/Types/DRKGauge.cs | 1 + .../ClientState/JobGauge/Types/PCTGauge.cs | 20 +- .../ClientState/JobGauge/Types/SMNGauge.cs | 1 + .../ClientState/JobGauge/Types/VPRGauge.cs | 12 +- .../Game/ClientState/Objects/TargetManager.cs | 28 +-- .../ClientState/Objects/Types/BattleChara.cs | 1 - .../ClientState/Objects/Types/Character.cs | 3 - Dalamud/Game/ClientState/Party/PartyList.cs | 2 +- Dalamud/Game/ClientState/Party/PartyMember.cs | 1 + .../Game/ClientState/Statuses/StatusList.cs | 2 +- Dalamud/Game/Command/CommandManager.cs | 8 +- Dalamud/Game/Config/GameConfig.cs | 4 +- Dalamud/Game/Config/GameConfigSection.cs | 4 +- Dalamud/Game/Framework.cs | 28 +-- Dalamud/Game/Gui/ChatGui.cs | 4 +- Dalamud/Game/Gui/ContextMenu/ContextMenu.cs | 5 +- Dalamud/Game/Gui/ContextMenu/MenuArgs.cs | 1 - Dalamud/Game/Gui/Dtr/DtrBar.cs | 6 +- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 3 +- Dalamud/Game/Gui/GameGui.cs | 2 +- .../Gui/NamePlate/NamePlateUpdateHandler.cs | 4 +- .../PartyFinder/Types/JobFlagsExtensions.cs | 1 + .../Gui/PartyFinder/Types/PartyFinderSlot.cs | 18 +- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 2 +- Dalamud/Game/Inventory/GameInventory.cs | 20 +- Dalamud/Game/Marketboard/MarketBoard.cs | 4 +- Dalamud/Game/NativeWrapper/AtkValuePtr.cs | 2 +- Dalamud/Game/Network/GameNetwork.cs | 3 - .../UniversalisMarketBoardUploader.cs | 4 +- .../Game/Network/Internal/NetworkHandlers.cs | 6 +- Dalamud/Game/SigScanner.cs | 2 + .../Game/Text/Evaluator/SeStringEvaluator.cs | 209 +++++------------- Dalamud/Game/Text/Noun/NounProcessor.cs | 2 +- Dalamud/Game/Text/SeStringHandling/Payload.cs | 5 +- .../SeStringHandling/Payloads/IconPayload.cs | 6 +- .../SeStringHandling/Payloads/ItemPayload.cs | 11 +- .../Payloads/MapLinkPayload.cs | 3 +- .../Payloads/NewLinePayload.cs | 2 +- .../Payloads/PartyFinderPayload.cs | 7 +- .../SeStringHandling/Payloads/QuestPayload.cs | 3 +- .../SeStringHandling/Payloads/RawPayload.cs | 2 +- .../Payloads/SeHyphenPayload.cs | 2 +- .../Payloads/StatusPayload.cs | 3 +- .../Payloads/UIForegroundPayload.cs | 7 +- .../Payloads/UIGlowPayload.cs | 7 +- .../Game/Text/SeStringHandling/SeString.cs | 2 +- Dalamud/Hooking/AsmHook.cs | 5 +- Dalamud/Hooking/Hook.cs | 7 +- .../Internal/FunctionPointerVariableHook.cs | 4 +- .../GameInteropProviderPluginScoped.cs | 5 +- Dalamud/Hooking/Internal/HookManager.cs | 5 +- Dalamud/Hooking/Internal/MinHookHook.cs | 2 +- .../Hooking/WndProcHook/WndProcHookManager.cs | 4 +- Dalamud/Interface/ColorHelpers.cs | 3 +- .../Components/ImGuiComponents.HelpMarker.cs | 1 + Dalamud/Interface/DragDrop/DragDropManager.cs | 1 + Dalamud/Interface/DragDrop/DragDropTarget.cs | 1 + .../FontAwesome/FontAwesomeExtensions.cs | 6 +- .../FontAwesome/FontAwesomeHelpers.cs | 13 +- .../DalamudAssetFontAndFamilyId.cs | 2 + .../DalamudDefaultFontAndFamilyId.cs | 2 + .../FontIdentifier/GameFontAndFamilyId.cs | 2 + .../Interface/FontIdentifier/IFontFamilyId.cs | 11 +- .../FontIdentifier/SingleFontSpec.cs | 1 + .../FontIdentifier/SystemFontFamilyId.cs | 4 +- .../Interface/FontIdentifier/SystemFontId.cs | 2 + Dalamud/Interface/GameFonts/FdtReader.cs | 6 +- .../Interface/GameFonts/GameFontLayoutPlan.cs | 45 ++-- Dalamud/Interface/GlyphRangesJapanese.cs | 6 +- .../ImGuiBackend/Dx11Win32Backend.cs | 7 - .../Renderers/Dx11Renderer.ViewportHandler.cs | 1 + .../ImGuiBackend/Renderers/Dx11Renderer.cs | 11 +- .../ImGuiFileDialog/FileDialog.Files.cs | 3 +- .../ImGuiFileDialog/FileDialog.Filters.cs | 15 +- .../ImGuiFileDialog/FileDialog.Helpers.cs | 2 +- .../ImGuiFileDialog/FileDialog.UI.cs | 20 +- .../Interface/ImGuiFileDialog/FileDialog.cs | 10 +- .../SingleFontChooserDialog.cs | 9 +- .../Internal/NotificationManager.cs | 4 +- .../ImGuiNotification/Notification.cs | 4 - .../NotificationUtilities.cs | 2 - .../Internal/SeStringRenderer.cs | 3 + .../SeStringDrawState.cs | 1 + .../Internal/Asserts/AssertHandler.cs | 2 +- Dalamud/Interface/Internal/DalamudCommands.cs | 9 +- Dalamud/Interface/Internal/DalamudIme.cs | 2 +- .../Interface/Internal/DalamudInterface.cs | 9 +- .../DalamudComponents.PluginPicker.cs | 3 +- .../ImGuiClipboardFunctionProvider.cs | 4 +- .../ImGuiInputTextStatePtrExtensions.cs | 2 +- .../Interface/Internal/InterfaceManager.cs | 13 +- .../Internal/PluginCategoryManager.cs | 8 +- Dalamud/Interface/Internal/UiDebug.cs | 9 +- .../UiDebug2/Browsing/AddonTree.AtkValues.cs | 3 +- .../UiDebug2/Browsing/AddonTree.FieldNames.cs | 4 +- .../Internal/UiDebug2/Browsing/AddonTree.cs | 1 - .../Internal/UiDebug2/Browsing/Events.cs | 1 + .../UiDebug2/Browsing/NodeTree.Component.cs | 1 + .../UiDebug2/Browsing/NodeTree.Editor.cs | 1 + .../UiDebug2/Browsing/NodeTree.Image.cs | 2 +- .../UiDebug2/Browsing/NodeTree.NineGrid.cs | 1 - .../UiDebug2/Browsing/NodeTree.Res.cs | 2 +- .../UiDebug2/Browsing/NodeTree.Text.cs | 1 + .../UiDebug2/Browsing/TimelineTree.cs | 1 + .../Internal/UiDebug2/ElementSelector.cs | 1 + .../Internal/UiDebug2/Popout.Node.cs | 1 + .../Internal/UiDebug2/UiDebug2.Sidebar.cs | 1 + .../Interface/Internal/UiDebug2/UiDebug2.cs | 7 +- .../Internal/UiDebug2/Utility/Gui.cs | 1 + .../Internal/UiDebug2/Utility/NodeBounds.cs | 1 + .../Internal/Windows/ChangelogWindow.cs | 4 +- .../Internal/Windows/ColorDemoWindow.cs | 1 - .../Internal/Windows/ComponentDemoWindow.cs | 12 +- .../Internal/Windows/ConsoleWindow.cs | 4 +- .../Internal/Windows/Data/DataWindow.cs | 5 +- .../Data/DataWindowWidgetExtensions.cs | 1 - .../Windows/Data/GameInventoryTestWidget.cs | 6 +- .../Internal/Windows/Data/WidgetUtil.cs | 1 - .../Data/Widgets/AddonInspectorWidget.cs | 2 +- .../Data/Widgets/AddonLifecycleWidget.cs | 2 +- .../Windows/Data/Widgets/AddressesWidget.cs | 4 +- .../Windows/Data/Widgets/AetherytesWidget.cs | 2 +- .../Data/Widgets/AtkArrayDataBrowserWidget.cs | 4 +- .../Windows/Data/Widgets/BuddyListWidget.cs | 2 +- .../Windows/Data/Widgets/CommandWidget.cs | 3 +- .../Windows/Data/Widgets/ConditionWidget.cs | 4 +- .../Data/Widgets/ConfigurationWidget.cs | 4 +- .../Windows/Data/Widgets/DataShareWidget.cs | 5 +- .../Windows/Data/Widgets/DtrBarWidget.cs | 4 +- .../Windows/Data/Widgets/FateTableWidget.cs | 3 +- .../Windows/Data/Widgets/FlyTextWidget.cs | 2 +- .../Data/Widgets/FontAwesomeTestWidget.cs | 2 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 8 +- .../Windows/Data/Widgets/GamepadWidget.cs | 4 +- .../Windows/Data/Widgets/GaugeWidget.cs | 2 +- .../Windows/Data/Widgets/HookWidget.cs | 5 +- .../Windows/Data/Widgets/IconBrowserWidget.cs | 4 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 28 +-- .../Windows/Data/Widgets/InventoryWidget.cs | 2 + .../Windows/Data/Widgets/KeyStateWidget.cs | 4 +- .../Windows/Data/Widgets/MarketBoardWidget.cs | 5 +- .../Data/Widgets/NetworkMonitorWidget.cs | 2 +- .../Data/Widgets/NounProcessorWidget.cs | 3 +- .../Windows/Data/Widgets/ObjectTableWidget.cs | 2 +- .../Windows/Data/Widgets/PartyListWidget.cs | 4 +- .../Windows/Data/Widgets/PluginIpcWidget.cs | 2 +- .../Windows/Data/Widgets/SeFontTestWidget.cs | 4 +- .../Widgets/SeStringRendererTestWidget.cs | 2 + .../Windows/Data/Widgets/ServicesWidget.cs | 18 +- .../Windows/Data/Widgets/StartInfoWidget.cs | 5 +- .../Windows/Data/Widgets/TargetWidget.cs | 2 +- .../Data/Widgets/TaskSchedulerWidget.cs | 4 +- .../Windows/Data/Widgets/TexWidget.cs | 11 +- .../Windows/Data/Widgets/ToastWidget.cs | 2 +- .../Windows/Data/Widgets/UIColorWidget.cs | 1 + .../Windows/Data/Widgets/UldWidget.cs | 1 + .../Windows/Data/Widgets/VfsWidget.cs | 3 +- .../Windows/GamepadModeNotifierWindow.cs | 1 + .../Internal/Windows/PluginImageCache.cs | 12 +- .../DalamudChangelogManager.cs | 3 +- .../PluginInstaller/PluginChangelogEntry.cs | 4 +- .../PluginInstaller/PluginInstallerWindow.cs | 13 +- .../PluginInstaller/ProfileManagerWidget.cs | 4 +- .../Internal/Windows/PluginStatWindow.cs | 6 +- .../Internal/Windows/ProfilerWindow.cs | 6 +- .../Windows/SelfTest/SelfTestWindow.cs | 7 +- .../Steps/AddonLifecycleSelfTestStep.cs | 8 +- .../SelfTest/Steps/CompletionSelfTestStep.cs | 1 - .../SelfTest/Steps/ConditionSelfTestStep.cs | 1 + .../SelfTest/Steps/ContextMenuSelfTestStep.cs | 2 + .../FrameworkTaskSchedulerSelfTestStep.cs | 2 - .../SelfTest/Steps/LuminaSelfTestStep.cs | 1 + .../SelfTest/Steps/MarketBoardSelfTestStep.cs | 1 - .../SelfTest/Steps/NamePlateSelfTestStep.cs | 2 +- .../SheetRedirectResolverSelfTestStep.cs | 1 + .../Windows/Settings/SettingsWindow.cs | 1 + .../Windows/Settings/Tabs/SettingsTabAbout.cs | 2 + .../Settings/Tabs/SettingsTabAutoUpdate.cs | 1 + .../Windows/Settings/Tabs/SettingsTabDtr.cs | 1 + .../Windows/Settings/Tabs/SettingsTabLook.cs | 1 + .../Settings/Widgets/EnumSettingsEntry{T}.cs | 2 - .../Widgets/ThirdRepoSettingsEntry.cs | 1 + .../Windows/StyleEditor/StyleEditorWindow.cs | 5 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 9 +- .../FontAtlasBuildToolkitUtilities.cs | 1 - .../Internals/DelegateFontHandle.cs | 9 +- .../FontAtlasFactory.BuildToolkit.cs | 22 +- .../FontAtlasFactory.Implementation.cs | 24 +- .../Internals/FontAtlasFactory.cs | 2 + .../ManagedFontAtlas/Internals/FontHandle.cs | 3 +- .../Internals/GamePrebakedFontHandle.cs | 23 +- .../Internals/LockedImFont.cs | 3 +- .../Internals/SimplePushedFont.cs | 1 - Dalamud/Interface/Style/DalamudColors.cs | 3 +- Dalamud/Interface/Style/StyleModel.cs | 22 +- Dalamud/Interface/Style/StyleModelV1.cs | 5 +- .../ForwardingSharedImmediateTexture.cs | 3 +- .../Textures/ISharedImmediateTexture.cs | 1 - .../Textures/ImGuiViewportTextureArgs.cs | 2 - .../ManifestResourceSharedImmediateTexture.cs | 6 +- .../SharedImmediateTexture.cs | 8 +- .../Internal/TextureManager.BlameTracker.cs | 5 +- .../Internal/TextureManager.Drawer.cs | 1 + .../Internal/TextureManager.GamePath.cs | 4 +- .../Internal/TextureManager.SharedTextures.cs | 3 +- .../Textures/Internal/TextureManager.cs | 6 +- .../TextureWraps/ForwardingTextureWrap.cs | 1 - .../Internal/ViewportTextureWrap.cs | 1 + .../TitleScreenMenu/TitleScreenMenu.cs | 14 +- Dalamud/Interface/UiBuilder.cs | 3 +- Dalamud/Interface/UldWrapper.cs | 3 +- .../Utility/Internal/DevTextureSaveMenu.cs | 2 + Dalamud/Interface/Utility/Raii/Color.cs | 3 +- Dalamud/Interface/Utility/Raii/EndObjects.cs | 4 +- Dalamud/Interface/Utility/Raii/Font.cs | 1 - Dalamud/Interface/Utility/Raii/Plot.cs | 4 +- Dalamud/Interface/Utility/Raii/Style.cs | 2 +- .../Windowing/Persistence/PresetModel.cs | 2 +- Dalamud/Interface/Windowing/Window.cs | 6 +- Dalamud/Interface/Windowing/WindowSystem.cs | 3 +- Dalamud/IoC/Internal/ObjectInstance.cs | 1 - Dalamud/IoC/Internal/ServiceContainer.cs | 6 +- Dalamud/Localization.cs | 2 +- Dalamud/Logging/Internal/TaskTracker.cs | 4 +- Dalamud/Memory/MemoryHelper.cs | 4 + .../Networking/Http/HappyEyeballsCallback.cs | 4 +- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- .../Internal/AutoUpdate/AutoUpdateManager.cs | 5 +- .../Loader/AssemblyLoadContextBuilder.cs | 8 +- .../Plugin/Internal/Loader/LoaderConfig.cs | 4 +- .../Internal/Loader/ManagedLoadContext.cs | 7 +- .../Internal/Loader/PlatformInformation.cs | 10 +- .../Plugin/Internal/Loader/PluginLoader.cs | 6 +- Dalamud/Plugin/Internal/PluginErrorHandler.cs | 9 +- Dalamud/Plugin/Internal/PluginManager.cs | 24 +- Dalamud/Plugin/Internal/PluginValidator.cs | 4 +- .../PluginManagementCommandHandler.cs | 4 +- Dalamud/Plugin/Internal/Profiles/Profile.cs | 4 +- .../Internal/Profiles/ProfileManager.cs | 22 +- .../Plugin/Internal/Profiles/ProfileModel.cs | 19 +- .../Internal/Profiles/ProfileModelV1.cs | 4 +- .../Plugin/Internal/Types/LocalDevPlugin.cs | 2 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- .../Types/Manifest/LocalPluginManifest.cs | 2 + .../Types/Manifest/RemotePluginManifest.cs | 1 + Dalamud/Plugin/Internal/Types/PluginDef.cs | 2 +- .../Plugin/Internal/Types/PluginManifest.cs | 1 + .../Plugin/Internal/Types/PluginRepository.cs | 10 +- Dalamud/Plugin/Ipc/ICallGateProvider.cs | 1 - Dalamud/Plugin/Ipc/Internal/CallGate.cs | 2 +- .../Plugin/Ipc/Internal/CallGateChannel.cs | 3 +- Dalamud/Plugin/Ipc/Internal/DataCache.cs | 2 +- Dalamud/Plugin/Ipc/Internal/DataShare.cs | 2 +- Dalamud/SafeMemory.cs | 4 +- Dalamud/Service/ServiceManager.cs | 20 +- Dalamud/Service/Service{T}.cs | 15 +- .../Storage/Assets/IDalamudAssetManager.cs | 3 +- Dalamud/Storage/ReliableFileStorage.cs | 13 +- Dalamud/Support/BugBait.cs | 1 + Dalamud/Support/Troubleshooting.cs | 6 +- Dalamud/Utility/ArrayExtensions.cs | 6 +- Dalamud/Utility/CultureFixes.cs | 4 +- Dalamud/Utility/DateTimeSpanExtensions.cs | 2 +- Dalamud/Utility/DiagnosticUtil.cs | 4 +- Dalamud/Utility/DisposeSafety.cs | 12 +- Dalamud/Utility/DynamicPriorityQueueLoader.cs | 2 +- Dalamud/Utility/EventHandlerExtensions.cs | 1 + Dalamud/Utility/FuzzyMatcher.cs | 18 +- Dalamud/Utility/Hash.cs | 7 +- Dalamud/Utility/RollingList.cs | 2 +- Dalamud/Utility/Signatures/SignatureHelper.cs | 7 +- Dalamud/Utility/StringExtensions.cs | 4 +- .../TerraFxComInterfaceExtensions.cs | 42 ++-- Dalamud/Utility/Timing/Timings.cs | 8 +- Dalamud/Utility/Util.cs | 10 +- Dalamud/Utility/WeakConcurrentCollection.cs | 4 +- 295 files changed, 830 insertions(+), 932 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index ddcb26914..7e93d2863 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.FontIdentifier; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Style; @@ -20,9 +19,12 @@ using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Storage; using Dalamud.Utility; + using Newtonsoft.Json; + using Serilog; using Serilog.Events; + using Windows.Win32.UI.WindowsAndMessaging; namespace Dalamud.Configuration.Internal; @@ -91,7 +93,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// /// Gets or sets a dictionary of seen FTUE levels. /// - public Dictionary SeenFtueLevels { get; set; } = new(); + public Dictionary SeenFtueLevels { get; set; } = []; /// /// Gets or sets the last loaded Dalamud version. @@ -111,7 +113,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// /// Gets or sets a list of custom repos. /// - public List ThirdRepoList { get; set; } = new(); + public List ThirdRepoList { get; set; } = []; /// /// Gets or sets a value indicating whether a disclaimer regarding third-party repos has been dismissed. @@ -121,12 +123,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// /// Gets or sets a list of hidden plugins. /// - public List HiddenPluginInternalName { get; set; } = new(); + public List HiddenPluginInternalName { get; set; } = []; /// /// Gets or sets a list of seen plugins. /// - public List SeenPluginInternalName { get; set; } = new(); + public List SeenPluginInternalName { get; set; } = []; /// /// Gets or sets a list of additional settings for devPlugins. The key is the absolute path @@ -134,14 +136,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// However by specifiying this value manually, you can add arbitrary files outside the normal /// file paths. /// - public Dictionary DevPluginSettings { get; set; } = new(); + public Dictionary DevPluginSettings { get; set; } = []; /// /// Gets or sets a list of additional locations that dev plugins should be loaded from. This can /// be either a DLL or folder, but should be the absolute path, or a path relative to the currently /// injected Dalamud instance. /// - public List DevPluginLoadLocations { get; set; } = new(); + public List DevPluginLoadLocations { get; set; } = []; /// /// Gets or sets the global UI scale. @@ -223,7 +225,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// /// Gets or sets a list representing the command history for the Dalamud Console. /// - public List LogCommandHistory { get; set; } = new(); + public List LogCommandHistory { get; set; } = []; /// /// Gets or sets a value indicating whether the dev bar should open at startup. @@ -599,7 +601,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService { // https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/animation/animation_win.cc;l=29?q=ReducedMotion&ss=chromium var winAnimEnabled = 0; - var success = false; + bool success; unsafe { success = Windows.Win32.PInvoke.SystemParametersInfo( diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs index 64327e658..400834cf8 100644 --- a/Dalamud/Configuration/Internal/DevPluginSettings.cs +++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs @@ -31,5 +31,5 @@ internal sealed class DevPluginSettings /// /// Gets or sets a list of validation problems that have been dismissed by the user. /// - public List DismissedValidationProblems { get; set; } = new(); + public List DismissedValidationProblems { get; set; } = []; } diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs index 377bac208..ba1c38b16 100644 --- a/Dalamud/Console/ConsoleManager.cs +++ b/Dalamud/Console/ConsoleManager.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; @@ -17,9 +17,9 @@ namespace Dalamud.Console; [ServiceManager.BlockingEarlyLoadedService("Console is needed by other blocking early loaded services.")] internal partial class ConsoleManager : IServiceType { - private static readonly ModuleLog Log = new("CON"); + private static readonly ModuleLog Log = ModuleLog.Create(); - private Dictionary entries = new(); + private Dictionary entries = []; /// /// Initializes a new instance of the class. @@ -99,10 +99,7 @@ internal partial class ConsoleManager : IServiceType ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(alias); - var target = this.FindEntry(name); - if (target == null) - throw new EntryNotFoundException(name); - + var target = this.FindEntry(name) ?? throw new EntryNotFoundException(name); if (this.FindEntry(alias) != null) throw new InvalidOperationException($"Entry '{alias}' already exists."); @@ -346,7 +343,7 @@ internal partial class ConsoleManager : IServiceType private static class Traits { - public static void ThrowIfTIsNullableAndNull(T? argument, [CallerArgumentExpression("argument")] string? paramName = null) + public static void ThrowIfTIsNullableAndNull(T? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { if (argument == null && !typeof(T).IsValueType) throw new ArgumentNullException(paramName); diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs index 41949c7d7..8e7516429 100644 --- a/Dalamud/Console/ConsoleManagerPluginScoped.cs +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -65,7 +65,7 @@ internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService [ServiceManager.ServiceDependency] private readonly ConsoleManager console = Service.Get(); - private readonly List trackedEntries = new(); + private readonly List trackedEntries = []; /// /// Initializes a new instance of the class. diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 2d32b8e8a..f80252ef9 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -14,7 +14,9 @@ using Dalamud.Plugin.Internal; using Dalamud.Storage; using Dalamud.Utility; using Dalamud.Utility.Timing; + using Serilog; + using Windows.Win32.Foundation; using Windows.Win32.Security; diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index f53195a2d..11cb3a979 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -8,11 +8,13 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using Dalamud.Utility.Timing; + using Lumina; using Lumina.Data; using Lumina.Excel; using Newtonsoft.Json; + using Serilog; namespace Dalamud.Data; diff --git a/Dalamud/Data/RsvResolver.cs b/Dalamud/Data/RsvResolver.cs index 3f507ff1d..6fd84356c 100644 --- a/Dalamud/Data/RsvResolver.cs +++ b/Dalamud/Data/RsvResolver.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using Dalamud.Hooking; using Dalamud.Logging.Internal; using Dalamud.Memory; + using FFXIVClientStructs.FFXIV.Client.LayoutEngine; + using Lumina.Text.ReadOnly; namespace Dalamud.Data; @@ -13,7 +15,7 @@ namespace Dalamud.Data; /// internal sealed unsafe class RsvResolver : IDisposable { - private static readonly ModuleLog Log = new("RsvProvider"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly Hook addRsvStringHook; diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index d9f6ef172..7abebee3b 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -1,8 +1,6 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Net; -using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -16,10 +14,13 @@ using Dalamud.Plugin.Internal; using Dalamud.Storage; using Dalamud.Support; using Dalamud.Utility; + using Newtonsoft.Json; + using Serilog; using Serilog.Core; using Serilog.Events; + using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; diff --git a/Dalamud/Game/Addon/Events/AddonEventEntry.cs b/Dalamud/Game/Addon/Events/AddonEventEntry.cs index 30d0465dc..eca1903f2 100644 --- a/Dalamud/Game/Addon/Events/AddonEventEntry.cs +++ b/Dalamud/Game/Addon/Events/AddonEventEntry.cs @@ -1,5 +1,4 @@ using Dalamud.Plugin.Services; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 980404940..17b70fcc2 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -24,7 +24,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService /// public static readonly Guid DalamudInternalKey = Guid.NewGuid(); - private static readonly ModuleLog Log = new("AddonEventManager"); + private static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycle = Service.Get(); diff --git a/Dalamud/Game/Addon/Events/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs index afaee9966..076c39cbb 100644 --- a/Dalamud/Game/Addon/Events/PluginEventController.cs +++ b/Dalamud/Game/Addon/Events/PluginEventController.cs @@ -5,7 +5,6 @@ using Dalamud.Game.Addon.Events.EventDataTypes; using Dalamud.Game.Gui; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -16,7 +15,7 @@ namespace Dalamud.Game.Addon.Events; /// internal unsafe class PluginEventController : IDisposable { - private static readonly ModuleLog Log = new("AddonEventManager"); + private static readonly ModuleLog Log = ModuleLog.Create(); /// /// Initializes a new instance of the class. @@ -28,7 +27,7 @@ internal unsafe class PluginEventController : IDisposable private AddonEventListener EventListener { get; init; } - private List Events { get; } = new(); + private List Events { get; } = []; /// /// Adds a tracked event. diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 78cea1a0f..64c005b5f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -25,7 +25,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// public static readonly List AllocatedTables = []; - private static readonly ModuleLog Log = new("AddonLifecycle"); + private static readonly ModuleLog Log = ModuleLog.Create(); private Hook? onInitializeAddonHook; diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index d41b1d9d8..1f20e3aeb 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -14,7 +14,7 @@ public abstract class BaseAddressResolver /// /// Gets a list of memory addresses that were found, to list in /xldata. /// - public static Dictionary> DebugScannedValues { get; } = new(); + public static Dictionary> DebugScannedValues { get; } = []; /// /// Gets or sets a value indicating whether the resolver has successfully run or . diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 279bf46e5..2d7d3c83a 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -1,5 +1,4 @@ using System.Linq; -using System.Reflection; using System.Text.RegularExpressions; using CheapLoc; @@ -23,7 +22,7 @@ namespace Dalamud.Game; [ServiceManager.EarlyLoadedService] internal partial class ChatHandlers : IServiceType { - private static readonly ModuleLog Log = new("ChatHandlers"); + private static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index 12a629958..2df64b73b 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -8,6 +8,7 @@ using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; + using Serilog; namespace Dalamud.Game.ClientState.Aetherytes; diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index f7c0b75ed..304dde82c 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -33,7 +33,7 @@ namespace Dalamud.Game.ClientState; [ServiceManager.EarlyLoadedService] internal sealed class ClientState : IInternalDisposableService, IClientState { - private static readonly ModuleLog Log = new("ClientState"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index ab4f8a03f..3a8642cfa 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -5,7 +5,9 @@ using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; + using FFXIVClientStructs.FFXIV.Client.System.Input; + using Serilog; namespace Dalamud.Game.ClientState.GamePad; diff --git a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs index 67429956b..5d6ba4554 100644 --- a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs +++ b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs @@ -37,7 +37,7 @@ internal class JobGauges : IServiceType, IJobGauges // Since the gauge itself reads from live memory, there isn't much downside to doing this. if (!this.cache.TryGetValue(typeof(T), out var gauge)) { - gauge = this.cache[typeof(T)] = (T)Activator.CreateInstance(typeof(T), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { this.Address }, null); + gauge = this.cache[typeof(T)] = (T)Activator.CreateInstance(typeof(T), BindingFlags.NonPublic | BindingFlags.Instance, null, [this.Address], null); } return (T)gauge; diff --git a/Dalamud/Game/ClientState/JobGauge/Types/BRDGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/BRDGauge.cs index 8880c3555..de73d540e 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/BRDGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/BRDGauge.cs @@ -1,4 +1,5 @@ using Dalamud.Game.ClientState.JobGauge.Enums; + using FFXIVClientStructs.FFXIV.Client.Game.Gauge; namespace Dalamud.Game.ClientState.JobGauge.Types; @@ -82,12 +83,12 @@ public unsafe class BRDGauge : JobGaugeBaseSongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.Mage : Song.None, this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonCoda) ? Song.Army : Song.None, this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.Wanderer : Song.None, - }; + ]; } } } diff --git a/Dalamud/Game/ClientState/JobGauge/Types/DRKGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/DRKGauge.cs index c56d03db0..06d923cc4 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/DRKGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/DRKGauge.cs @@ -1,4 +1,5 @@ using Dalamud.Game.ClientState.JobGauge.Enums; + using FFXIVClientStructs.FFXIV.Client.Game.Gauge; namespace Dalamud.Game.ClientState.JobGauge.Types; diff --git a/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs index d31a22702..9745bff7c 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using CanvasFlags = Dalamud.Game.ClientState.JobGauge.Enums.CanvasFlags; using CreatureFlags = Dalamud.Game.ClientState.JobGauge.Enums.CreatureFlags; @@ -22,45 +22,45 @@ public unsafe class PCTGauge : JobGaugeBase /// /// Gets the use of subjective pallete. /// - public byte PalleteGauge => Struct->PalleteGauge; + public byte PalleteGauge => this.Struct->PalleteGauge; /// /// Gets the amount of paint the player has. /// - public byte Paint => Struct->Paint; + public byte Paint => this.Struct->Paint; /// /// Gets a value indicating whether a creature motif is drawn. /// - public bool CreatureMotifDrawn => Struct->CreatureMotifDrawn; + public bool CreatureMotifDrawn => this.Struct->CreatureMotifDrawn; /// /// Gets a value indicating whether a weapon motif is drawn. /// - public bool WeaponMotifDrawn => Struct->WeaponMotifDrawn; + public bool WeaponMotifDrawn => this.Struct->WeaponMotifDrawn; /// /// Gets a value indicating whether a landscape motif is drawn. /// - public bool LandscapeMotifDrawn => Struct->LandscapeMotifDrawn; + public bool LandscapeMotifDrawn => this.Struct->LandscapeMotifDrawn; /// /// Gets a value indicating whether a moogle portrait is ready. /// - public bool MooglePortraitReady => Struct->MooglePortraitReady; + public bool MooglePortraitReady => this.Struct->MooglePortraitReady; /// /// Gets a value indicating whether a madeen portrait is ready. /// - public bool MadeenPortraitReady => Struct->MadeenPortraitReady; + public bool MadeenPortraitReady => this.Struct->MadeenPortraitReady; /// /// Gets which creature flags are present. /// - public CreatureFlags CreatureFlags => (CreatureFlags)Struct->CreatureFlags; + public CreatureFlags CreatureFlags => (CreatureFlags)this.Struct->CreatureFlags; /// /// Gets which canvas flags are present. /// - public CanvasFlags CanvasFlags => (CanvasFlags)Struct->CanvasFlags; + public CanvasFlags CanvasFlags => (CanvasFlags)this.Struct->CanvasFlags; } diff --git a/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs index 899ea78eb..5f2d6e932 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/SMNGauge.cs @@ -1,4 +1,5 @@ using Dalamud.Game.ClientState.JobGauge.Enums; + using FFXIVClientStructs.FFXIV.Client.Game.Gauge; namespace Dalamud.Game.ClientState.JobGauge.Types; diff --git a/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs index 3c822c7d7..625ecde24 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs @@ -1,7 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game.Gauge; -using Reloaded.Memory; - using DreadCombo = Dalamud.Game.ClientState.JobGauge.Enums.DreadCombo; using SerpentCombo = Dalamud.Game.ClientState.JobGauge.Enums.SerpentCombo; @@ -24,25 +22,25 @@ public unsafe class VPRGauge : JobGaugeBase /// /// Gets how many uses of uncoiled fury the player has. /// - public byte RattlingCoilStacks => Struct->RattlingCoilStacks; + public byte RattlingCoilStacks => this.Struct->RattlingCoilStacks; /// /// Gets Serpent Offering stacks and gauge. /// - public byte SerpentOffering => Struct->SerpentOffering; + public byte SerpentOffering => this.Struct->SerpentOffering; /// /// Gets value indicating the use of 1st, 2nd, 3rd, 4th generation and Ouroboros. /// - public byte AnguineTribute => Struct->AnguineTribute; + public byte AnguineTribute => this.Struct->AnguineTribute; /// /// Gets the last Weaponskill used in DreadWinder/Pit of Dread combo. /// - public DreadCombo DreadCombo => (DreadCombo)Struct->DreadCombo; + public DreadCombo DreadCombo => (DreadCombo)this.Struct->DreadCombo; /// /// Gets current ability for Serpent's Tail. /// - public SerpentCombo SerpentCombo => (SerpentCombo)Struct->SerpentCombo; + public SerpentCombo SerpentCombo => (SerpentCombo)this.Struct->SerpentCombo; } diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index a6432e242..5f317d077 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -30,50 +30,50 @@ internal sealed unsafe class TargetManager : IServiceType, ITargetManager /// public IGameObject? Target { - get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetHardTarget()); - set => Struct->SetHardTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address); + get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GetHardTarget()); + set => this.Struct->SetHardTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address); } /// public IGameObject? MouseOverTarget { - get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget); - set => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; + get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->MouseOverTarget); + set => this.Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } /// public IGameObject? FocusTarget { - get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget); - set => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; + get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->FocusTarget); + set => this.Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } /// public IGameObject? PreviousTarget { - get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget); - set => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; + get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->PreviousTarget); + set => this.Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } /// public IGameObject? SoftTarget { - get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetSoftTarget()); - set => Struct->SetSoftTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address); + get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GetSoftTarget()); + set => this.Struct->SetSoftTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address); } /// public IGameObject? GPoseTarget { - get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget); - set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; + get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GPoseTarget); + set => this.Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } /// public IGameObject? MouseOverNameplateTarget { - get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverNameplateTarget); - set => Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; + get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->MouseOverNameplateTarget); + set => this.Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } private TargetSystem* Struct => TargetSystem.Instance(); diff --git a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs index 238c81a72..37f1f5504 100644 --- a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs +++ b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs @@ -1,5 +1,4 @@ using Dalamud.Game.ClientState.Statuses; -using Dalamud.Utility; namespace Dalamud.Game.ClientState.Objects.Types; diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index a91ecc230..2002a16b8 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -1,9 +1,6 @@ -using System.Runtime.CompilerServices; - using Dalamud.Data; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Memory; using Lumina.Excel; using Lumina.Excel.Sheets; diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 90959f926..0326350d2 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -47,7 +47,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance(); /// - public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); + public nint GroupListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.PartyMembers[0]); /// public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); diff --git a/Dalamud/Game/ClientState/Party/PartyMember.cs b/Dalamud/Game/ClientState/Party/PartyMember.cs index 84e3f21c8..843824318 100644 --- a/Dalamud/Game/ClientState/Party/PartyMember.cs +++ b/Dalamud/Game/ClientState/Party/PartyMember.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.Text.SeStringHandling; + using Dalamud.Utility; using Lumina.Excel; diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index 43650a48c..2e8024ab8 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -38,7 +38,7 @@ public sealed unsafe partial class StatusList /// /// Gets the amount of status effect slots the actor has. /// - public int Length => Struct->NumValidStatuses; + public int Length => this.Struct->NumValidStatuses; private static int StatusSize { get; } = Marshal.SizeOf(); diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 01442c409..e9abb7336 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -24,7 +24,7 @@ namespace Dalamud.Game.Command; [ServiceManager.EarlyLoadedService] internal sealed unsafe class CommandManager : IInternalDisposableService, ICommandManager { - private static readonly ModuleLog Log = new("Command"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly ConcurrentDictionary commandMap = new(); private readonly ConcurrentDictionary<(string, IReadOnlyCommandInfo), string> commandAssemblyNameMap = new(); @@ -71,7 +71,7 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma if (separatorPosition + 1 >= content.Length) { // Remove the trailing space - command = content.Substring(0, separatorPosition); + command = content[..separatorPosition]; } else { @@ -262,12 +262,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma #pragma warning restore SA1015 internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager { - private static readonly ModuleLog Log = new("Command"); + private static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceDependency] private readonly CommandManager commandManagerService = Service.Get(); - private readonly List pluginRegisteredCommands = new(); + private readonly List pluginRegisteredCommands = []; private readonly LocalPlugin pluginInfo; /// diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 9579d84bc..a55056351 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -1,11 +1,13 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Common.Configuration; + using Serilog; namespace Dalamud.Game.Config; diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs index 8ebab8a60..eb2f1107e 100644 --- a/Dalamud/Game/Config/GameConfigSection.cs +++ b/Dalamud/Game/Config/GameConfigSection.cs @@ -1,10 +1,12 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Diagnostics; using System.Text; using Dalamud.Memory; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Common.Configuration; + using Serilog; namespace Dalamud.Game.Config; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 808bbce50..035745684 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -26,7 +26,7 @@ namespace Dalamud.Game; [ServiceManager.EarlyLoadedService] internal sealed class Framework : IInternalDisposableService, IFramework { - private static readonly ModuleLog Log = new("Framework"); + private static readonly ModuleLog Log = ModuleLog.Create(); private static readonly Stopwatch StatsStopwatch = new(); @@ -86,7 +86,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework /// /// Gets the stats history mapping. /// - public static Dictionary> StatsHistory { get; } = new(); + public static Dictionary> StatsHistory { get; } = []; /// public DateTime LastUpdate { get; private set; } = DateTime.MinValue; @@ -106,7 +106,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework /// /// Gets the list of update sub-delegates that didn't get updated this frame. /// - internal List NonUpdatedSubDelegates { get; private set; } = new(); + internal List NonUpdatedSubDelegates { get; private set; } = []; /// /// Gets or sets a value indicating whether to dispatch update events. @@ -212,11 +212,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.ContinueWhenAll( - new[] - { + [ Task.Delay(delay, cancellationToken), this.DelayTicks(delayTicks, cancellationToken), - }, + ], _ => func(), cancellationToken, TaskContinuationOptions.HideScheduler, @@ -239,11 +238,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.ContinueWhenAll( - new[] - { + [ Task.Delay(delay, cancellationToken), this.DelayTicks(delayTicks, cancellationToken), - }, + ], _ => action(), cancellationToken, TaskContinuationOptions.HideScheduler, @@ -266,11 +264,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.ContinueWhenAll( - new[] - { + [ Task.Delay(delay, cancellationToken), this.DelayTicks(delayTicks, cancellationToken), - }, + ], _ => func(), cancellationToken, TaskContinuationOptions.HideScheduler, @@ -293,11 +290,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.ContinueWhenAll( - new[] - { + [ Task.Delay(delay, cancellationToken), this.DelayTicks(delayTicks, cancellationToken), - }, + ], _ => func(), cancellationToken, TaskContinuationOptions.HideScheduler, @@ -333,7 +329,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework internal static void AddToStats(string key, double ms) { if (!StatsHistory.ContainsKey(key)) - StatsHistory.Add(key, new List()); + StatsHistory.Add(key, []); StatsHistory[key].Add(ms); diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 30e2b676c..e9a0a1aae 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -37,10 +37,10 @@ namespace Dalamud.Game.Gui; [ServiceManager.EarlyLoadedService] internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui { - private static readonly ModuleLog Log = new("ChatGui"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly Queue chatQueue = new(); - private readonly Dictionary<(string PluginName, uint CommandId), Action> dalamudLinkHandlers = new(); + private readonly Dictionary<(string PluginName, uint CommandId), Action> dalamudLinkHandlers = []; private readonly Hook printMessageHook; private readonly Hook inventoryItemCopyHook; diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs index aada374ec..0b306f093 100644 --- a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; @@ -28,7 +29,7 @@ namespace Dalamud.Game.Gui.ContextMenu; [ServiceManager.EarlyLoadedService] internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu { - private static readonly ModuleLog Log = new("ContextMenu"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly Hook atkModuleVf22OpenAddonByAgentHook; private readonly Hook addonContextMenuOnMenuSelectedHook; @@ -53,7 +54,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM private Dictionary> MenuItems { get; } = []; - private object MenuItemsLock { get; } = new(); + private Lock MenuItemsLock { get; } = new(); private AgentInterface* SelectedAgent { get; set; } diff --git a/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs b/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs index 39fd1c52c..900935ed5 100644 --- a/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs +++ b/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; -using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI.Agent; diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 2235c5ade..5663d0748 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -30,7 +30,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar { private const uint BaseNodeId = 1000; - private static readonly ModuleLog Log = new("DtrBar"); + private static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -54,7 +54,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar private readonly ReaderWriterLockSlim entriesLock = new(); private readonly List entries = []; - private readonly Dictionary> eventHandles = new(); + private readonly Dictionary> eventHandles = []; private ImmutableList? entriesReadOnlyCopy; @@ -516,7 +516,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar var node = data.TextNode = this.MakeNode(++this.runningNodeIds); - this.eventHandles.TryAdd(node->NodeId, new List()); + this.eventHandles.TryAdd(node->NodeId, []); this.eventHandles[node->NodeId].AddRange(new List { this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler), diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index 138484580..e0bd8fd49 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -1,7 +1,6 @@ -using System.Numerics; +using System.Numerics; using Dalamud.Configuration.Internal; -using Dalamud.Game.Addon.Events.EventDataTypes; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 3d17aad86..3b0f6eb66 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -32,7 +32,7 @@ namespace Dalamud.Game.Gui; [ServiceManager.EarlyLoadedService] internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui { - private static readonly ModuleLog Log = new("GameGui"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly GameGuiAddressResolver address; diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs index a8c6ff3c1..e197e2360 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs @@ -427,8 +427,8 @@ internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler /// public int VisibilityFlags { - get => ObjectData->VisibilityFlags; - set => ObjectData->VisibilityFlags = value; + get => this.ObjectData->VisibilityFlags; + set => this.ObjectData->VisibilityFlags = value; } /// diff --git a/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs b/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs index 1c78c871b..d7ab3080b 100644 --- a/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs +++ b/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin.Services; + using Lumina.Excel.Sheets; namespace Dalamud.Game.Gui.PartyFinder.Types; diff --git a/Dalamud/Game/Gui/PartyFinder/Types/PartyFinderSlot.cs b/Dalamud/Game/Gui/PartyFinder/Types/PartyFinderSlot.cs index 3d1e496fc..953e575d4 100644 --- a/Dalamud/Game/Gui/PartyFinder/Types/PartyFinderSlot.cs +++ b/Dalamud/Game/Gui/PartyFinder/Types/PartyFinderSlot.cs @@ -23,23 +23,7 @@ public class PartyFinderSlot /// /// Gets a list of jobs that this slot is accepting. /// - public IReadOnlyCollection Accepting - { - get - { - if (this.listAccepting != null) - { - return this.listAccepting; - } - - this.listAccepting = Enum.GetValues(typeof(JobFlags)) - .Cast() - .Where(flag => this[flag]) - .ToArray(); - - return this.listAccepting; - } - } + public IReadOnlyCollection Accepting => this.listAccepting ??= Enum.GetValues().Where(flag => this[flag]).ToArray(); /// /// Tests if this slot is accepting a job. diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 466401ef3..24fa88023 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -22,7 +22,7 @@ namespace Dalamud.Game.Internal; [ServiceManager.EarlyLoadedService] internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService { - private static readonly ModuleLog Log = new("DalamudAtkTweaks"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly Hook hookAgentHudOpenSystemMenu; diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 5390c2707..2cfacfad0 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -18,15 +18,15 @@ namespace Dalamud.Game.Inventory; [ServiceManager.EarlyLoadedService] internal class GameInventory : IInternalDisposableService { - private readonly List subscribersPendingChange = new(); - private readonly List subscribers = new(); + private readonly List subscribersPendingChange = []; + private readonly List subscribers = []; - private readonly List addedEvents = new(); - private readonly List removedEvents = new(); - private readonly List changedEvents = new(); - private readonly List movedEvents = new(); - private readonly List splitEvents = new(); - private readonly List mergedEvents = new(); + private readonly List addedEvents = []; + private readonly List removedEvents = []; + private readonly List changedEvents = []; + private readonly List movedEvents = []; + private readonly List splitEvents = []; + private readonly List mergedEvents = []; [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -151,7 +151,7 @@ internal class GameInventory : IInternalDisposableService bool isNew; lock (this.subscribersPendingChange) { - isNew = this.subscribersPendingChange.Any() && !this.subscribers.Any(); + isNew = this.subscribersPendingChange.Count != 0 && this.subscribers.Count == 0; this.subscribers.Clear(); this.subscribers.AddRange(this.subscribersPendingChange); this.subscribersChanged = false; @@ -348,7 +348,7 @@ internal class GameInventory : IInternalDisposableService #pragma warning restore SA1015 internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInventory { - private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped)); + private static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceDependency] private readonly GameInventory gameInventoryService = Service.Get(); diff --git a/Dalamud/Game/Marketboard/MarketBoard.cs b/Dalamud/Game/Marketboard/MarketBoard.cs index 962e0010e..563a5bc4a 100644 --- a/Dalamud/Game/Marketboard/MarketBoard.cs +++ b/Dalamud/Game/Marketboard/MarketBoard.cs @@ -1,5 +1,3 @@ -using System.Linq; - using Dalamud.Game.Network.Internal; using Dalamud.Game.Network.Structures; using Dalamud.IoC; @@ -95,7 +93,7 @@ internal class MarketBoard : IInternalDisposableService, IMarketBoard #pragma warning restore SA1015 internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard { - private static readonly ModuleLog Log = new(nameof(MarketBoardPluginScoped)); + private static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceDependency] private readonly MarketBoard marketBoardService = Service.Get(); diff --git a/Dalamud/Game/NativeWrapper/AtkValuePtr.cs b/Dalamud/Game/NativeWrapper/AtkValuePtr.cs index a47483a66..b274d388b 100644 --- a/Dalamud/Game/NativeWrapper/AtkValuePtr.cs +++ b/Dalamud/Game/NativeWrapper/AtkValuePtr.cs @@ -89,7 +89,7 @@ public readonly unsafe struct AtkValuePtr(nint address) : IEquatable public unsafe bool TryGet([NotNullWhen(true)] out T? result) where T : struct { - object? value = this.GetValue(); + var value = this.GetValue(); if (value is T typed) { result = typed; diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index be464ef34..b8c91b235 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -2,9 +2,6 @@ using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; using Dalamud.Hooking; -using Dalamud.IoC; -using Dalamud.IoC.Internal; -using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Network; diff --git a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs index 7ecb9c397..67769e4ca 100644 --- a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs +++ b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -8,6 +7,7 @@ using Dalamud.Game.Network.Structures; using Dalamud.Networking.Http; using Newtonsoft.Json; + using Serilog; namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis; @@ -64,7 +64,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader PricePerUnit = marketBoardItemListing.PricePerUnit, Quantity = marketBoardItemListing.ItemQuantity, RetainerCity = marketBoardItemListing.RetainerCityId, - Materia = new List(), + Materia = [], }; #pragma warning restore CS0618 // Type or member is obsolete diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 6a6d73b33..5ca7da54a 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -33,7 +33,7 @@ namespace Dalamud.Game.Network.Internal; [ServiceManager.EarlyLoadedService] internal unsafe class NetworkHandlers : IInternalDisposableService { - private readonly IMarketBoardUploader uploader; + private readonly UniversalisMarketBoardUploader uploader; private readonly IDisposable handleMarketBoardItemRequest; private readonly IDisposable handleMarketTaxRates; @@ -419,7 +419,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService private IDisposable HandleMarketBoardItemRequest() { - void LogStartObserved(MarketBoardItemRequest request) + static void LogStartObserved(MarketBoardItemRequest request) { Log.Verbose("Observed start of request for item with {NumListings} expected listings", request.AmountToArrive); } @@ -448,7 +448,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService private void UploadMarketBoardData( MarketBoardItemRequest request, (uint CatalogId, ICollection Sales) sales, - ICollection listings, + List listings, ulong uploaderId, uint worldId) { diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index 262e98fa5..81fb8a3a3 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -11,7 +11,9 @@ using System.Threading; using Dalamud.Plugin.Services; using Iced.Intel; + using Newtonsoft.Json; + using Serilog; namespace Dalamud.Game; diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index f05c15263..d16b3b9b3 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -48,7 +48,7 @@ namespace Dalamud.Game.Text.Evaluator; [ResolveVia] internal class SeStringEvaluator : IServiceType, ISeStringEvaluator { - private static readonly ModuleLog Log = new("SeStringEvaluator"); + private static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceDependency] private readonly ClientState.ClientState clientState = Service.Get(); @@ -244,154 +244,67 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator // if (context.HandlePayload(payload, in context)) // return true; - switch (payload.MacroCode) + return payload.MacroCode switch { - case MacroCode.SetResetTime: - return this.TryResolveSetResetTime(in context, payload); - - case MacroCode.SetTime: - return this.TryResolveSetTime(in context, payload); - - case MacroCode.If: - return this.TryResolveIf(in context, payload); - - case MacroCode.Switch: - return this.TryResolveSwitch(in context, payload); - - case MacroCode.SwitchPlatform: - return this.TryResolveSwitchPlatform(in context, payload); - - case MacroCode.PcName: - return this.TryResolvePcName(in context, payload); - - case MacroCode.IfPcGender: - return this.TryResolveIfPcGender(in context, payload); - - case MacroCode.IfPcName: - return this.TryResolveIfPcName(in context, payload); - - // case MacroCode.Josa: - // case MacroCode.Josaro: - - case MacroCode.IfSelf: - return this.TryResolveIfSelf(in context, payload); - - // case MacroCode.NewLine: // pass through - // case MacroCode.Wait: // pass through - // case MacroCode.Icon: // pass through - - case MacroCode.Color: - return this.TryResolveColor(in context, payload); - - case MacroCode.EdgeColor: - return this.TryResolveEdgeColor(in context, payload); - - case MacroCode.ShadowColor: - return this.TryResolveShadowColor(in context, payload); - - // case MacroCode.SoftHyphen: // pass through - // case MacroCode.Key: - // case MacroCode.Scale: - - case MacroCode.Bold: - return this.TryResolveBold(in context, payload); - - case MacroCode.Italic: - return this.TryResolveItalic(in context, payload); - - // case MacroCode.Edge: - // case MacroCode.Shadow: - // case MacroCode.NonBreakingSpace: // pass through - // case MacroCode.Icon2: // pass through - // case MacroCode.Hyphen: // pass through - - case MacroCode.Num: - return this.TryResolveNum(in context, payload); - - case MacroCode.Hex: - return this.TryResolveHex(in context, payload); - - case MacroCode.Kilo: - return this.TryResolveKilo(in context, payload); - - // case MacroCode.Byte: - - case MacroCode.Sec: - return this.TryResolveSec(in context, payload); - - // case MacroCode.Time: - - case MacroCode.Float: - return this.TryResolveFloat(in context, payload); - - // case MacroCode.Link: // pass through - - case MacroCode.Sheet: - return this.TryResolveSheet(in context, payload); - - case MacroCode.SheetSub: - return this.TryResolveSheetSub(in context, payload); - - case MacroCode.String: - return this.TryResolveString(in context, payload); - - case MacroCode.Caps: - return this.TryResolveCaps(in context, payload); - - case MacroCode.Head: - return this.TryResolveHead(in context, payload); - - case MacroCode.Split: - return this.TryResolveSplit(in context, payload); - - case MacroCode.HeadAll: - return this.TryResolveHeadAll(in context, payload); - - case MacroCode.Fixed: - return this.TryResolveFixed(in context, payload); - - case MacroCode.Lower: - return this.TryResolveLower(in context, payload); - - case MacroCode.JaNoun: - return this.TryResolveNoun(ClientLanguage.Japanese, in context, payload); - - case MacroCode.EnNoun: - return this.TryResolveNoun(ClientLanguage.English, in context, payload); - - case MacroCode.DeNoun: - return this.TryResolveNoun(ClientLanguage.German, in context, payload); - - case MacroCode.FrNoun: - return this.TryResolveNoun(ClientLanguage.French, in context, payload); - - // case MacroCode.ChNoun: - - case MacroCode.LowerHead: - return this.TryResolveLowerHead(in context, payload); - - case MacroCode.ColorType: - return this.TryResolveColorType(in context, payload); - - case MacroCode.EdgeColorType: - return this.TryResolveEdgeColorType(in context, payload); - - // case MacroCode.Ruby: - - case MacroCode.Digit: - return this.TryResolveDigit(in context, payload); - - case MacroCode.Ordinal: - return this.TryResolveOrdinal(in context, payload); - - // case MacroCode.Sound: // pass through - - case MacroCode.LevelPos: - return this.TryResolveLevelPos(in context, payload); - - default: - return false; - } + MacroCode.SetResetTime => this.TryResolveSetResetTime(in context, payload), + MacroCode.SetTime => this.TryResolveSetTime(in context, payload), + MacroCode.If => this.TryResolveIf(in context, payload), + MacroCode.Switch => this.TryResolveSwitch(in context, payload), + MacroCode.SwitchPlatform => this.TryResolveSwitchPlatform(in context, payload), + MacroCode.PcName => this.TryResolvePcName(in context, payload), + MacroCode.IfPcGender => this.TryResolveIfPcGender(in context, payload), + MacroCode.IfPcName => this.TryResolveIfPcName(in context, payload), + // MacroCode.Josa + // MacroCode.Josaro + MacroCode.IfSelf => this.TryResolveIfSelf(in context, payload), + // MacroCode.NewLine (pass through) + // MacroCode.Wait (pass through) + // MacroCode.Icon (pass through) + MacroCode.Color => this.TryResolveColor(in context, payload), + MacroCode.EdgeColor => this.TryResolveEdgeColor(in context, payload), + MacroCode.ShadowColor => this.TryResolveShadowColor(in context, payload), + // MacroCode.SoftHyphen (pass through) + // MacroCode.Key + // MacroCode.Scale + MacroCode.Bold => this.TryResolveBold(in context, payload), + MacroCode.Italic => this.TryResolveItalic(in context, payload), + // MacroCode.Edge + // MacroCode.Shadow + // MacroCode.NonBreakingSpace (pass through) + // MacroCode.Icon2 (pass through) + // MacroCode.Hyphen (pass through) + MacroCode.Num => this.TryResolveNum(in context, payload), + MacroCode.Hex => this.TryResolveHex(in context, payload), + MacroCode.Kilo => this.TryResolveKilo(in context, payload), + // MacroCode.Byte + MacroCode.Sec => this.TryResolveSec(in context, payload), + // MacroCode.Time + MacroCode.Float => this.TryResolveFloat(in context, payload), + // MacroCode.Link (pass through) + MacroCode.Sheet => this.TryResolveSheet(in context, payload), + MacroCode.SheetSub => this.TryResolveSheetSub(in context, payload), + MacroCode.String => this.TryResolveString(in context, payload), + MacroCode.Caps => this.TryResolveCaps(in context, payload), + MacroCode.Head => this.TryResolveHead(in context, payload), + MacroCode.Split => this.TryResolveSplit(in context, payload), + MacroCode.HeadAll => this.TryResolveHeadAll(in context, payload), + MacroCode.Fixed => this.TryResolveFixed(in context, payload), + MacroCode.Lower => this.TryResolveLower(in context, payload), + MacroCode.JaNoun => this.TryResolveNoun(ClientLanguage.Japanese, in context, payload), + MacroCode.EnNoun => this.TryResolveNoun(ClientLanguage.English, in context, payload), + MacroCode.DeNoun => this.TryResolveNoun(ClientLanguage.German, in context, payload), + MacroCode.FrNoun => this.TryResolveNoun(ClientLanguage.French, in context, payload), + // MacroCode.ChNoun + MacroCode.LowerHead => this.TryResolveLowerHead(in context, payload), + MacroCode.ColorType => this.TryResolveColorType(in context, payload), + MacroCode.EdgeColorType => this.TryResolveEdgeColorType(in context, payload), + // MacroCode.Ruby + MacroCode.Digit => this.TryResolveDigit(in context, payload), + MacroCode.Ordinal => this.TryResolveOrdinal(in context, payload), + // MacroCode.Sound (pass through) + MacroCode.LevelPos => this.TryResolveLevelPos(in context, payload), + _ => false, + }; } private unsafe bool TryResolveSetResetTime(in SeStringContext context, in ReadOnlySePayloadSpan payload) diff --git a/Dalamud/Game/Text/Noun/NounProcessor.cs b/Dalamud/Game/Text/Noun/NounProcessor.cs index 993d341df..c218bb26c 100644 --- a/Dalamud/Game/Text/Noun/NounProcessor.cs +++ b/Dalamud/Game/Text/Noun/NounProcessor.cs @@ -85,7 +85,7 @@ internal class NounProcessor : IServiceType private const int PronounColumnIdx = 6; private const int ArticleColumnIdx = 7; - private static readonly ModuleLog Log = new("NounProcessor"); + private static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service.Get(); diff --git a/Dalamud/Game/Text/SeStringHandling/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs index c797c4a91..685ff517a 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs @@ -213,11 +213,10 @@ public abstract partial class Payload return payload; } - private static Payload DecodeText(BinaryReader reader) + private static TextPayload DecodeText(BinaryReader reader) { var payload = new TextPayload(); payload.DecodeImpl(reader, reader.BaseStream.Length); - return payload; } } @@ -382,7 +381,7 @@ public abstract partial class Payload { if (value < 0xCF) { - return new byte[] { (byte)(value + 1) }; + return [(byte)(value + 1)]; } var bytes = BitConverter.GetBytes(value); diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/IconPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/IconPayload.cs index 7963d422f..ad25de01d 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/IconPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/IconPayload.cs @@ -45,10 +45,10 @@ public class IconPayload : Payload { var indexBytes = MakeInteger((uint)this.Icon); var chunkLen = indexBytes.Length + 1; - var bytes = new List(new byte[] - { + var bytes = new List( + [ START_BYTE, (byte)SeStringChunkType.Icon, (byte)chunkLen, - }); + ]); bytes.AddRange(indexBytes); bytes.Add(END_BYTE); return bytes.ToArray(); diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs index 0c1f75a1d..20f05958f 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs @@ -8,6 +8,7 @@ using Dalamud.Utility; using Lumina.Excel; using Lumina.Excel.Sheets; + using Newtonsoft.Json; namespace Dalamud.Game.Text.SeStringHandling.Payloads; @@ -172,7 +173,7 @@ public class ItemPayload : Payload }; bytes.AddRange(idBytes); // unk - bytes.AddRange(new byte[] { 0x02, 0x01 }); + bytes.AddRange([0x02, 0x01]); // Links don't have to include the name, but if they do, it requires additional work if (hasName) @@ -183,17 +184,17 @@ public class ItemPayload : Payload nameLen += 4; // space plus 3 bytes for HQ symbol } - bytes.AddRange(new byte[] - { + bytes.AddRange( + [ 0xFF, // unk (byte)nameLen, - }); + ]); bytes.AddRange(Encoding.UTF8.GetBytes(this.displayName)); if (this.IsHQ) { // space and HQ symbol - bytes.AddRange(new byte[] { 0x20, 0xEE, 0x80, 0xBC }); + bytes.AddRange([0x20, 0xEE, 0x80, 0xBC]); } } diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs index 7b672d07a..31cab41a1 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs @@ -5,6 +5,7 @@ using Dalamud.Data; using Lumina.Excel; using Lumina.Excel.Sheets; + using Newtonsoft.Json; namespace Dalamud.Game.Text.SeStringHandling.Payloads; @@ -174,7 +175,7 @@ public class MapLinkPayload : Payload bytes.AddRange(yBytes); // unk - bytes.AddRange(new byte[] { 0xFF, 0x01, END_BYTE }); + bytes.AddRange([0xFF, 0x01, END_BYTE]); return bytes.ToArray(); } diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs index 0b090b3d6..8072c87f5 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs @@ -7,7 +7,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads; /// public class NewLinePayload : Payload, ITextProvider { - private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.NewLine, 0x01, END_BYTE }; + private readonly byte[] bytes = [START_BYTE, (byte)SeStringChunkType.NewLine, 0x01, END_BYTE]; /// /// Gets an instance of NewLinePayload. diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/PartyFinderPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/PartyFinderPayload.cs index 0931ab03f..77ed7b8bc 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/PartyFinderPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/PartyFinderPayload.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using Lumina.Extensions; + using Newtonsoft.Json; namespace Dalamud.Game.Text.SeStringHandling.Payloads @@ -97,7 +98,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads reader.ReadByte(); // if the next byte is 0xF3 then this listing is limited to home world - byte nextByte = reader.ReadByte(); + var nextByte = reader.ReadByte(); switch (nextByte) { case (byte)PartyFinderLinkType.LimitedToHomeWorld: @@ -121,11 +122,11 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads // if the link type is notification, just use premade payload data since it's always the same. // i have no idea why it is formatted like this, but it is how it is. // note it is identical to the link terminator payload except the embedded info type is 0x08 - if (this.LinkType == PartyFinderLinkType.PartyFinderNotification) return new byte[] { 0x02, 0x27, 0x07, 0x08, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03, }; + if (this.LinkType == PartyFinderLinkType.PartyFinderNotification) return [0x02, 0x27, 0x07, 0x08, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03,]; // back to our regularly scheduled programming... var listingIDBytes = MakeInteger(this.ListingId); - bool isFlagSpecified = this.LinkType != PartyFinderLinkType.NotSpecified; + var isFlagSpecified = this.LinkType != PartyFinderLinkType.NotSpecified; var chunkLen = listingIDBytes.Length + 4; // 1 more byte for the type flag if it is specified diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/QuestPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/QuestPayload.cs index 19d494d8a..47947ccde 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/QuestPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/QuestPayload.cs @@ -5,6 +5,7 @@ using Dalamud.Data; using Lumina.Excel; using Lumina.Excel.Sheets; + using Newtonsoft.Json; namespace Dalamud.Game.Text.SeStringHandling.Payloads; @@ -62,7 +63,7 @@ public class QuestPayload : Payload }; bytes.AddRange(idBytes); - bytes.AddRange(new byte[] { 0x01, 0x01, END_BYTE }); + bytes.AddRange([0x01, 0x01, END_BYTE]); return bytes.ToArray(); } diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs index 02a7c113e..50464077b 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs @@ -45,7 +45,7 @@ public class RawPayload : Payload /// /// Gets a fixed Payload representing a common link-termination sequence, found in many payload chains. /// - public static RawPayload LinkTerminator => new(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 }); + public static RawPayload LinkTerminator => new([0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]); /// public override PayloadType Type => PayloadType.Unknown; diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/SeHyphenPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/SeHyphenPayload.cs index 1739b9cda..48c55a5a8 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/SeHyphenPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/SeHyphenPayload.cs @@ -7,7 +7,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads; /// public class SeHyphenPayload : Payload, ITextProvider { - private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.SeHyphen, 0x01, END_BYTE }; + private readonly byte[] bytes = [START_BYTE, (byte)SeStringChunkType.SeHyphen, 0x01, END_BYTE]; /// /// Gets an instance of SeHyphenPayload. diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/StatusPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/StatusPayload.cs index d102dfab6..c17213f60 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/StatusPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/StatusPayload.cs @@ -5,6 +5,7 @@ using Dalamud.Data; using Lumina.Excel; using Lumina.Excel.Sheets; + using Newtonsoft.Json; namespace Dalamud.Game.Text.SeStringHandling.Payloads; @@ -63,7 +64,7 @@ public class StatusPayload : Payload bytes.AddRange(idBytes); // unk - bytes.AddRange(new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x20, END_BYTE }); + bytes.AddRange([0x01, 0x01, 0xFF, 0x02, 0x20, END_BYTE]); return bytes.ToArray(); } diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs index bf360ce34..f161cff9d 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs @@ -5,6 +5,7 @@ using Dalamud.Data; using Lumina.Excel; using Lumina.Excel.Sheets; + using Newtonsoft.Json; namespace Dalamud.Game.Text.SeStringHandling.Payloads; @@ -95,10 +96,10 @@ public class UIForegroundPayload : Payload var colorBytes = MakeInteger(this.colorKey); var chunkLen = colorBytes.Length + 1; - var bytes = new List(new byte[] - { + var bytes = new List( + [ START_BYTE, (byte)SeStringChunkType.UIForeground, (byte)chunkLen, - }); + ]); bytes.AddRange(colorBytes); bytes.Add(END_BYTE); diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs index e54427073..01c0d05d2 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs @@ -5,6 +5,7 @@ using Dalamud.Data; using Lumina.Excel; using Lumina.Excel.Sheets; + using Newtonsoft.Json; namespace Dalamud.Game.Text.SeStringHandling.Payloads; @@ -98,10 +99,10 @@ public class UIGlowPayload : Payload var colorBytes = MakeInteger(this.colorKey); var chunkLen = colorBytes.Length + 1; - var bytes = new List(new byte[] - { + var bytes = new List( + [ START_BYTE, (byte)SeStringChunkType.UIGlow, (byte)chunkLen, - }); + ]); bytes.AddRange(colorBytes); bytes.Add(END_BYTE); diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index b94b05cac..b64a8bb67 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -28,7 +28,7 @@ public class SeString /// public SeString() { - this.Payloads = new List(); + this.Payloads = []; } /// diff --git a/Dalamud/Hooking/AsmHook.cs b/Dalamud/Hooking/AsmHook.cs index 09ae336dc..e6c9f61d8 100644 --- a/Dalamud/Hooking/AsmHook.cs +++ b/Dalamud/Hooking/AsmHook.cs @@ -169,9 +169,6 @@ public sealed class AsmHook : IDisposable, IDalamudHook /// private void CheckDisposed() { - if (this.IsDisposed) - { - throw new ObjectDisposedException(message: "Hook is already disposed", null); - } + ObjectDisposedException.ThrowIf(this.IsDisposed, this); } } diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index b8fd78b4f..c54a8f399 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -160,7 +160,7 @@ public abstract class Hook : IDalamudHook where T : Delegate (int)Math.Min(pDataDirectory->Size + pDataDirectory->VirtualAddress - importDescriptor.Name, moduleNameLowerWithNullTerminator.Length)); // Is this entry about the DLL that we're looking for? (Case insensitive) - if (currentDllNameWithNullTerminator.ToLowerInvariant() != moduleNameLowerWithNullTerminator) + if (!currentDllNameWithNullTerminator.Equals(moduleNameLowerWithNullTerminator, StringComparison.InvariantCultureIgnoreCase)) continue; if (isPe64) @@ -245,10 +245,7 @@ public abstract class Hook : IDalamudHook where T : Delegate /// protected void CheckDisposed() { - if (this.IsDisposed) - { - throw new ObjectDisposedException(message: "Hook is already disposed", null); - } + ObjectDisposedException.ThrowIf(this.IsDisposed, this); } private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) diff --git a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs index 8a53e664a..3d1073d2f 100644 --- a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs +++ b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; using System.Reflection; using System.Runtime.InteropServices; using JetBrains.Annotations; + using Windows.Win32.System.Memory; using Win32Exception = System.ComponentModel.Win32Exception; @@ -45,7 +45,7 @@ internal unsafe class FunctionPointerVariableHook : Hook if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) { - indexList = HookManager.MultiHookTracker[this.Address] = new List(); + indexList = HookManager.MultiHookTracker[this.Address] = []; } this.detourDelegate = detour; diff --git a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index 52d266335..583060d53 100644 --- a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Linq; using Dalamud.Game; @@ -8,6 +8,7 @@ using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; using Dalamud.Utility.Signatures; + using Serilog; namespace Dalamud.Hooking.Internal; @@ -25,7 +26,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IInternal private readonly LocalPlugin plugin; private readonly SigScanner scanner; - private readonly WeakConcurrentCollection trackedHooks = new(); + private readonly WeakConcurrentCollection trackedHooks = []; /// /// Initializes a new instance of the class. diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs index 8f5dba583..83af01bf4 100644 --- a/Dalamud/Hooking/Internal/HookManager.cs +++ b/Dalamud/Hooking/Internal/HookManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using Dalamud.Logging.Internal; using Dalamud.Memory; @@ -20,7 +21,7 @@ internal class HookManager : IInternalDisposableService /// /// Logger shared with . /// - internal static readonly ModuleLog Log = new("HM"); + internal static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceConstructor] private HookManager() @@ -30,7 +31,7 @@ internal class HookManager : IInternalDisposableService /// /// Gets sync root object for hook enabling/disabling. /// - internal static object HookEnableSyncRoot { get; } = new(); + internal static Lock HookEnableSyncRoot { get; } = new(); /// /// Gets a static list of tracked and registered hooks. diff --git a/Dalamud/Hooking/Internal/MinHookHook.cs b/Dalamud/Hooking/Internal/MinHookHook.cs index d4889ba11..9c7ec0f88 100644 --- a/Dalamud/Hooking/Internal/MinHookHook.cs +++ b/Dalamud/Hooking/Internal/MinHookHook.cs @@ -24,7 +24,7 @@ internal class MinHookHook : Hook where T : Delegate var unhooker = HookManager.RegisterUnhooker(this.Address); if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) - indexList = HookManager.MultiHookTracker[this.Address] = new(); + indexList = HookManager.MultiHookTracker[this.Address] = []; var index = (ulong)indexList.Count; diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs index 7c70a9f0b..82620eed8 100644 --- a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -17,10 +17,10 @@ namespace Dalamud.Hooking.WndProcHook; [ServiceManager.EarlyLoadedService] internal sealed class WndProcHookManager : IInternalDisposableService { - private static readonly ModuleLog Log = new(nameof(WndProcHookManager)); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly Hook dispatchMessageWHook; - private readonly Dictionary wndProcOverrides = new(); + private readonly Dictionary wndProcOverrides = []; private HWND mainWindowHwnd; diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs index a3ae6799e..dd56f3d14 100644 --- a/Dalamud/Interface/ColorHelpers.cs +++ b/Dalamud/Interface/ColorHelpers.cs @@ -56,11 +56,10 @@ public static class ColorHelpers var min = Math.Min(r, Math.Min(g, b)); var h = max; - var s = max; var v = max; var d = max - min; - s = max == 0 ? 0 : d / max; + var s = max == 0 ? 0 : d / max; if (max == min) { diff --git a/Dalamud/Interface/Components/ImGuiComponents.HelpMarker.cs b/Dalamud/Interface/Components/ImGuiComponents.HelpMarker.cs index 57a4bd150..0d919cf6e 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.HelpMarker.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.HelpMarker.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Common.Math; namespace Dalamud.Interface.Components; diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 4375ddea9..504bdc716 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -5,6 +5,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; + using Serilog; namespace Dalamud.Interface.DragDrop; diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index c6b66e7e8..8ac347c72 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -6,6 +6,7 @@ using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.DragDrop; diff --git a/Dalamud/Interface/FontAwesome/FontAwesomeExtensions.cs b/Dalamud/Interface/FontAwesome/FontAwesomeExtensions.cs index 2b8cb8c52..0bab4538a 100644 --- a/Dalamud/Interface/FontAwesome/FontAwesomeExtensions.cs +++ b/Dalamud/Interface/FontAwesome/FontAwesomeExtensions.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Utility; @@ -37,7 +37,7 @@ public static class FontAwesomeExtensions public static IEnumerable GetSearchTerms(this FontAwesomeIcon icon) { var searchTermsAttribute = icon.GetAttribute(); - return searchTermsAttribute == null ? new string[] { } : searchTermsAttribute.SearchTerms; + return searchTermsAttribute == null ? [] : searchTermsAttribute.SearchTerms; } /// @@ -48,6 +48,6 @@ public static class FontAwesomeExtensions public static IEnumerable GetCategories(this FontAwesomeIcon icon) { var categoriesAttribute = icon.GetAttribute(); - return categoriesAttribute == null ? new string[] { } : categoriesAttribute.Categories; + return categoriesAttribute == null ? [] : categoriesAttribute.Categories; } } diff --git a/Dalamud/Interface/FontAwesome/FontAwesomeHelpers.cs b/Dalamud/Interface/FontAwesome/FontAwesomeHelpers.cs index cf34d736e..51c896ad2 100644 --- a/Dalamud/Interface/FontAwesome/FontAwesomeHelpers.cs +++ b/Dalamud/Interface/FontAwesome/FontAwesomeHelpers.cs @@ -16,14 +16,7 @@ public static class FontAwesomeHelpers /// list of font awesome icons. public static List GetIcons() { - var icons = new List(); - foreach (var icon in Enum.GetValues(typeof(FontAwesomeIcon)).Cast().ToList()) - { - if (icon.IsObsolete()) continue; - icons.Add(icon); - } - - return icons; + return [.. Enum.GetValues().Where(icon => !icon.IsObsolete())]; } /// @@ -75,7 +68,7 @@ public static class FontAwesomeHelpers { var name = Enum.GetName(icon)?.ToLowerInvariant(); var searchTerms = icon.GetSearchTerms(); - if (name!.Contains(search.ToLowerInvariant()) || searchTerms.Contains(search.ToLowerInvariant())) + if (name!.Contains(search, StringComparison.InvariantCultureIgnoreCase) || searchTerms.Contains(search.ToLowerInvariant())) { result.Add(icon); } @@ -105,7 +98,7 @@ public static class FontAwesomeHelpers var name = Enum.GetName(icon)?.ToLowerInvariant(); var searchTerms = icon.GetSearchTerms(); var categories = icon.GetCategories(); - if ((name!.Contains(search.ToLowerInvariant()) || searchTerms.Contains(search.ToLowerInvariant())) && categories.Contains(category)) + if ((name!.Contains(search, StringComparison.InvariantCultureIgnoreCase) || searchTerms.Contains(search.ToLowerInvariant())) && categories.Contains(category)) { result.Add(icon); } diff --git a/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs index c531dced5..3778ea0de 100644 --- a/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs +++ b/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Storage.Assets; + using Newtonsoft.Json; + using TerraFX.Interop.DirectX; namespace Dalamud.Interface.FontIdentifier; diff --git a/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs index c45cf256b..4666de54a 100644 --- a/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs +++ b/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ManagedFontAtlas; + using Newtonsoft.Json; + using TerraFX.Interop.DirectX; namespace Dalamud.Interface.FontIdentifier; diff --git a/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs index e294c8813..f19a2ec6a 100644 --- a/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs +++ b/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using Dalamud.Bindings.ImGui; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; + using Newtonsoft.Json; + using TerraFX.Interop.DirectX; namespace Dalamud.Interface.FontIdentifier; diff --git a/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs index 991716f74..40780422a 100644 --- a/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs +++ b/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs @@ -36,26 +36,25 @@ public interface IFontFamilyId : IObjectWithLocalizableName /// /// The list of fonts. public static List ListDalamudFonts() => - new() - { + [ new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium), new DalamudAssetFontAndFamilyId(DalamudAsset.InconsolataRegular), new DalamudAssetFontAndFamilyId(DalamudAsset.FontAwesomeFreeSolid), - }; + ]; /// /// Gets the list of Game-provided fonts. /// /// The list of fonts. - public static List ListGameFonts() => new() - { + public static List ListGameFonts() => + [ new GameFontAndFamilyId(GameFontFamily.Axis), new GameFontAndFamilyId(GameFontFamily.Jupiter), new GameFontAndFamilyId(GameFontFamily.JupiterNumeric), new GameFontAndFamilyId(GameFontFamily.Meidinger), new GameFontAndFamilyId(GameFontFamily.MiedingerMid), new GameFontAndFamilyId(GameFontFamily.TrumpGothic), - }; + ]; /// /// Gets the list of System-provided fonts. diff --git a/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs index 070b1c1e1..b1c03f9dd 100644 --- a/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs +++ b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs @@ -6,6 +6,7 @@ using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; + using Newtonsoft.Json; namespace Dalamud.Interface.FontIdentifier; diff --git a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs index 83a5e810d..60e20fb6f 100644 --- a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs +++ b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs @@ -83,7 +83,7 @@ public sealed class SystemFontFamilyId : IFontFamilyId else if (candidates.Any(x => x.Style == (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL)) candidates.RemoveAll(x => x.Style != (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL); - if (!candidates.Any()) + if (candidates.Count == 0) return 0; for (var i = 0; i < this.Fonts.Count; i++) @@ -117,7 +117,7 @@ public sealed class SystemFontFamilyId : IFontFamilyId return new(IObjectWithLocalizableName.GetLocaleNames(fn)); } - private unsafe IReadOnlyList GetFonts() + private unsafe List GetFonts() { using var dwf = default(ComPtr); fixed (Guid* piid = &IID.IID_IDWriteFactory) diff --git a/Dalamud/Interface/FontIdentifier/SystemFontId.cs b/Dalamud/Interface/FontIdentifier/SystemFontId.cs index 8401f4c79..45885a9a8 100644 --- a/Dalamud/Interface/FontIdentifier/SystemFontId.cs +++ b/Dalamud/Interface/FontIdentifier/SystemFontId.cs @@ -5,7 +5,9 @@ using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Utility; + using Newtonsoft.Json; + using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; diff --git a/Dalamud/Interface/GameFonts/FdtReader.cs b/Dalamud/Interface/GameFonts/FdtReader.cs index 0e8f3fb59..6f6aaea25 100644 --- a/Dalamud/Interface/GameFonts/FdtReader.cs +++ b/Dalamud/Interface/GameFonts/FdtReader.cs @@ -43,12 +43,12 @@ public class FdtReader /// /// Gets all the glyphs defined in this file. /// - public List Glyphs { get; init; } = new(); + public List Glyphs { get; init; } = []; /// /// Gets all the kerning entries defined in this file. /// - public List Distances { get; init; } = new(); + public List Distances { get; init; } = []; /// /// Finds the glyph index for the corresponding codepoint. @@ -269,7 +269,7 @@ public class FdtReader /// /// Mapping of texture channel index to byte index. /// - public static readonly int[] TextureChannelOrder = { 2, 1, 0, 3 }; + public static readonly int[] TextureChannelOrder = [2, 1, 0, 3]; /// /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian. diff --git a/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs index 5b0fe100b..126e7601f 100644 --- a/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs +++ b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs @@ -200,30 +200,25 @@ public class GameFontLayoutPlan return false; // TODO: Whatever - switch (char.GetUnicodeCategory((char)this.Codepoint)) - { - case System.Globalization.UnicodeCategory.SpaceSeparator: - case System.Globalization.UnicodeCategory.LineSeparator: - case System.Globalization.UnicodeCategory.ParagraphSeparator: - case System.Globalization.UnicodeCategory.Control: - case System.Globalization.UnicodeCategory.Format: - case System.Globalization.UnicodeCategory.Surrogate: - case System.Globalization.UnicodeCategory.PrivateUse: - case System.Globalization.UnicodeCategory.ConnectorPunctuation: - case System.Globalization.UnicodeCategory.DashPunctuation: - case System.Globalization.UnicodeCategory.OpenPunctuation: - case System.Globalization.UnicodeCategory.ClosePunctuation: - case System.Globalization.UnicodeCategory.InitialQuotePunctuation: - case System.Globalization.UnicodeCategory.FinalQuotePunctuation: - case System.Globalization.UnicodeCategory.OtherPunctuation: - case System.Globalization.UnicodeCategory.MathSymbol: - case System.Globalization.UnicodeCategory.ModifierSymbol: - case System.Globalization.UnicodeCategory.OtherSymbol: - case System.Globalization.UnicodeCategory.OtherNotAssigned: - return true; - } - - return false; + return char.GetUnicodeCategory((char)this.Codepoint) + is System.Globalization.UnicodeCategory.SpaceSeparator + or System.Globalization.UnicodeCategory.LineSeparator + or System.Globalization.UnicodeCategory.ParagraphSeparator + or System.Globalization.UnicodeCategory.Control + or System.Globalization.UnicodeCategory.Format + or System.Globalization.UnicodeCategory.Surrogate + or System.Globalization.UnicodeCategory.PrivateUse + or System.Globalization.UnicodeCategory.ConnectorPunctuation + or System.Globalization.UnicodeCategory.DashPunctuation + or System.Globalization.UnicodeCategory.OpenPunctuation + or System.Globalization.UnicodeCategory.ClosePunctuation + or System.Globalization.UnicodeCategory.InitialQuotePunctuation + or System.Globalization.UnicodeCategory.FinalQuotePunctuation + or System.Globalization.UnicodeCategory.OtherPunctuation + or System.Globalization.UnicodeCategory.MathSymbol + or System.Globalization.UnicodeCategory.ModifierSymbol + or System.Globalization.UnicodeCategory.OtherSymbol + or System.Globalization.UnicodeCategory.OtherNotAssigned; } } } @@ -300,7 +295,7 @@ public class GameFontLayoutPlan elements.Add(new() { Codepoint = c, Glyph = this.fdt.GetGlyph(c), }); var lastBreakIndex = 0; - List lineBreakIndices = new() { 0 }; + List lineBreakIndices = [0]; for (var i = 1; i < elements.Count; i++) { var prev = elements[i - 1]; diff --git a/Dalamud/Interface/GlyphRangesJapanese.cs b/Dalamud/Interface/GlyphRangesJapanese.cs index 2773b9db5..2acf7f40e 100644 --- a/Dalamud/Interface/GlyphRangesJapanese.cs +++ b/Dalamud/Interface/GlyphRangesJapanese.cs @@ -8,8 +8,8 @@ public static class GlyphRangesJapanese /// /// Gets the unicode glyph ranges for the Japanese language. /// - public static ushort[] GlyphRanges => new ushort[] - { + public static ushort[] GlyphRanges => + [ 0x0020, 0x00FF, 0x0391, 0x03A1, 0x03A3, 0x03A9, 0x03B1, 0x03C1, 0x03C3, 0x03C9, 0x0401, 0x0401, 0x0410, 0x044F, 0x0451, 0x0451, 0x2000, 0x206F, 0x2103, 0x2103, 0x212B, 0x212B, 0x2190, 0x2193, 0x21D2, 0x21D2, 0x21D4, 0x21D4, 0x2200, 0x2200, 0x2202, 0x2203, 0x2207, 0x2208, 0x220B, 0x220B, 0x2212, 0x2212, 0x221A, 0x221A, 0x221D, 0x221E, 0x2220, 0x2220, 0x2227, 0x222C, 0x2234, 0x2235, @@ -524,5 +524,5 @@ public static class GlyphRangesJapanese 0x9F4E, 0x9F4F, 0x9F52, 0x9F52, 0x9F54, 0x9F54, 0x9F5F, 0x9F63, 0x9F66, 0x9F67, 0x9F6A, 0x9F6A, 0x9F6C, 0x9F6C, 0x9F72, 0x9F72, 0x9F76, 0x9F77, 0x9F8D, 0x9F8D, 0x9F95, 0x9F95, 0x9F9C, 0x9F9D, 0x9FA0, 0x9FA0, 0xFF01, 0xFF01, 0xFF03, 0xFF06, 0xFF08, 0xFF0C, 0xFF0E, 0xFF3B, 0xFF3D, 0xFF5D, 0xFF61, 0xFF9F, 0xFFE3, 0xFFE3, 0xFFE5, 0xFFE5, 0xFFFF, 0xFFFF, 0, - }; + ]; } diff --git a/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs b/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs index ea609828d..e3b98ec37 100644 --- a/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs +++ b/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs @@ -1,9 +1,4 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGuizmo; @@ -14,8 +9,6 @@ using Dalamud.Interface.ImGuiBackend.InputHandler; using Dalamud.Interface.ImGuiBackend.Renderers; using Dalamud.Utility; -using Serilog; - using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; diff --git a/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.ViewportHandler.cs b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.ViewportHandler.cs index fe83d58a9..c8d82648e 100644 --- a/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.ViewportHandler.cs +++ b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.ViewportHandler.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ImGuiBackend.Helpers; using Dalamud.Utility; + using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; diff --git a/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs index 2eb4e4970..a6abbb862 100644 --- a/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs +++ b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs @@ -15,6 +15,7 @@ using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Interface.Utility; using Dalamud.Utility; + using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -30,7 +31,7 @@ namespace Dalamud.Interface.ImGuiBackend.Renderers; Justification = "Multiple fixed/using scopes")] internal unsafe partial class Dx11Renderer : IImGuiRenderer { - private readonly List fontTextures = new(); + private readonly List fontTextures = []; private readonly D3D_FEATURE_LEVEL featureLevel; private readonly ViewportHandler viewportHandler; private readonly nint renderNamePtr; @@ -398,10 +399,9 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer /// private void CreateFontsTexture() { - if (this.device.IsEmpty()) - throw new ObjectDisposedException(nameof(Dx11Renderer)); + ObjectDisposedException.ThrowIf(this.device.IsEmpty(), this); - if (this.fontTextures.Any()) + if (this.fontTextures.Count != 0) return; var io = ImGui.GetIO(); @@ -479,8 +479,7 @@ internal unsafe partial class Dx11Renderer : IImGuiRenderer /// private void EnsureDeviceObjects() { - if (this.device.IsEmpty()) - throw new ObjectDisposedException(nameof(Dx11Renderer)); + ObjectDisposedException.ThrowIf(this.device.IsEmpty(), this); var assembly = Assembly.GetExecutingAssembly(); diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs index e5b7fc15e..1bad08f08 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using Dalamud.Utility; @@ -11,7 +12,7 @@ namespace Dalamud.Interface.ImGuiFileDialog; /// public partial class FileDialog { - private readonly object filesLock = new(); + private readonly Lock filesLock = new(); private readonly DriveListLoader driveListLoader = new(); diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Filters.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Filters.cs index 85b380bee..46f264354 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Filters.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Filters.cs @@ -8,11 +8,12 @@ namespace Dalamud.Interface.ImGuiFileDialog; /// public partial class FileDialog { - private static Regex filterRegex = new(@"[^,{}]+(\{([^{}]*?)\})?", RegexOptions.Compiled); - - private List filters = new(); + private List filters = []; private FilterStruct selectedFilter; + [GeneratedRegex(@"[^,{}]+(\{([^{}]*?)\})?", RegexOptions.Compiled)] + private static partial Regex FilterRegex(); + private void ParseFilters(string filters) { // ".*,.cpp,.h,.hpp" @@ -22,13 +23,13 @@ public partial class FileDialog if (filters.Length == 0) return; var currentFilterFound = false; - var matches = filterRegex.Matches(filters); + var matches = FilterRegex().Matches(filters); foreach (Match m in matches) { var match = m.Value; var filter = default(FilterStruct); - if (match.Contains("{")) + if (match.Contains('{')) { var exts = m.Groups[2].Value; filter = new FilterStruct @@ -42,7 +43,7 @@ public partial class FileDialog filter = new FilterStruct { Filter = match, - CollectionFilters = new(), + CollectionFilters = [], }; } @@ -89,7 +90,7 @@ public partial class FileDialog foreach (var file in this.files) { var show = true; - if (!string.IsNullOrEmpty(this.searchBuffer) && !file.FileName.ToLowerInvariant().Contains(this.searchBuffer.ToLowerInvariant())) + if (!string.IsNullOrEmpty(this.searchBuffer) && !file.FileName.Contains(this.searchBuffer, StringComparison.InvariantCultureIgnoreCase)) { show = false; } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Helpers.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Helpers.cs index 57844c48b..7e5363673 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Helpers.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Helpers.cs @@ -12,7 +12,7 @@ public partial class FileDialog private static string BytesToString(long byteCount) { - string[] suf = { " B", " KB", " MB", " GB", " TB" }; + string[] suf = [" B", " KB", " MB", " GB", " TB"]; if (byteCount == 0) return "0" + suf[0]; var bytes = Math.Abs(byteCount); diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index 3c02f9559..ef886e957 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -54,7 +54,7 @@ public partial class FileDialog windowVisible = ImGui.Begin(name, ref this.visible, this.WindowFlags); } - bool wasClosed = false; + var wasClosed = false; if (windowVisible) { if (!this.visible) @@ -122,15 +122,15 @@ public partial class FileDialog { if (iconMap == null) { - iconMap = new(); - AddToIconMap(new[] { "mp4", "gif", "mov", "avi" }, FontAwesomeIcon.FileVideo, miscTextColor); - AddToIconMap(new[] { "pdf" }, FontAwesomeIcon.FilePdf, miscTextColor); - AddToIconMap(new[] { "png", "jpg", "jpeg", "tiff" }, FontAwesomeIcon.FileImage, imageTextColor); - AddToIconMap(new[] { "cs", "json", "cpp", "h", "py", "xml", "yaml", "js", "html", "css", "ts", "java" }, FontAwesomeIcon.FileCode, codeTextColor); - AddToIconMap(new[] { "txt", "md" }, FontAwesomeIcon.FileAlt, standardTextColor); - AddToIconMap(new[] { "zip", "7z", "gz", "tar" }, FontAwesomeIcon.FileArchive, miscTextColor); - AddToIconMap(new[] { "mp3", "m4a", "ogg", "wav" }, FontAwesomeIcon.FileAudio, miscTextColor); - AddToIconMap(new[] { "csv" }, FontAwesomeIcon.FileCsv, miscTextColor); + iconMap = []; + AddToIconMap(["mp4", "gif", "mov", "avi"], FontAwesomeIcon.FileVideo, miscTextColor); + AddToIconMap(["pdf"], FontAwesomeIcon.FilePdf, miscTextColor); + AddToIconMap(["png", "jpg", "jpeg", "tiff"], FontAwesomeIcon.FileImage, imageTextColor); + AddToIconMap(["cs", "json", "cpp", "h", "py", "xml", "yaml", "js", "html", "css", "ts", "java"], FontAwesomeIcon.FileCode, codeTextColor); + AddToIconMap(["txt", "md"], FontAwesomeIcon.FileAlt, standardTextColor); + AddToIconMap(["zip", "7z", "gz", "tar"], FontAwesomeIcon.FileArchive, miscTextColor); + AddToIconMap(["mp3", "m4a", "ogg", "wav"], FontAwesomeIcon.FileAudio, miscTextColor); + AddToIconMap(["csv"], FontAwesomeIcon.FileCsv, miscTextColor); } return iconMap.TryGetValue(ext.ToLowerInvariant(), out var icon) ? icon : new IconColorItem diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs index 3d8246ffd..e33fc2fc4 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs @@ -30,7 +30,7 @@ public partial class FileDialog private string currentPath; private string fileNameBuffer = string.Empty; - private List pathDecomposition = new(); + private List pathDecomposition = []; private bool pathClicked = true; private bool pathInputActivated = false; private string pathInputBuffer = string.Empty; @@ -46,12 +46,12 @@ public partial class FileDialog private string searchBuffer = string.Empty; private string lastSelectedFileName = string.Empty; - private List selectedFileNames = new(); + private List selectedFileNames = []; private float footerHeight = 0; private string selectedSideBar = string.Empty; - private List quickAccess = new(); + private List quickAccess = []; /// /// Initializes a new instance of the class. @@ -130,12 +130,12 @@ public partial class FileDialog { if (!this.flags.HasFlag(ImGuiFileDialogFlags.SelectOnly)) { - return new List { this.GetFilePathName() }; + return [this.GetFilePathName()]; } if (this.IsDirectoryMode() && this.selectedFileNames.Count == 0) { - return new List { this.GetFilePathName() }; // current directory + return [this.GetFilePathName()]; // current directory } var fullPaths = this.selectedFileNames.Where(x => !string.IsNullOrEmpty(x)).Select(x => Path.Combine(this.currentPath, x)); diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs index 6a381f5b2..2abdd3403 100644 --- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -13,6 +13,7 @@ using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; + using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -31,10 +32,10 @@ public sealed class SingleFontChooserDialog : IDisposable private const float MaxFontSizePt = 127; - private static readonly List EmptyIFontList = new(); + private static readonly List EmptyIFontList = []; private static readonly (string Name, float Value)[] FontSizeList = - { + [ ("9.6", 9.6f), ("10", 10f), ("12", 12f), @@ -51,7 +52,7 @@ public sealed class SingleFontChooserDialog : IDisposable ("46", 46), ("68", 68), ("90", 90), - }; + ]; private static int counterStatic; @@ -1235,7 +1236,7 @@ public sealed class SingleFontChooserDialog : IDisposable } private void UpdateSelectedFamilyAndFontIndices( - IReadOnlyList fonts, + List fonts, string familyName, string fontName) { diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index 340763a55..1b6e7e59b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -26,8 +26,8 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - private readonly List notifications = new(); - private readonly ConcurrentBag pendingNotifications = new(); + private readonly List notifications = []; + private readonly ConcurrentBag pendingNotifications = []; private NotificationPositionChooser? positionChooser; diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 4dcb10c17..ef6bb3da8 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,9 +1,5 @@ -using System.Threading.Tasks; - using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Textures; -using Dalamud.Interface.Textures.TextureWraps; -using Serilog; namespace Dalamud.Interface.ImGuiNotification; /// Represents a blueprint for a notification. diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 4f0830fa1..5ef3cf4ac 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -1,11 +1,9 @@ using System.IO; using System.Numerics; using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; -using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Textures; diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index f161c1868..5a19f9a50 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using System.Text; using BitFaster.Caching.Lru; + using Dalamud.Bindings.ImGui; using Dalamud.Data; using Dalamud.Game; @@ -12,8 +13,10 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing; using Dalamud.Interface.Utility; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI; + using Lumina.Excel.Sheets; using Lumina.Text; using Lumina.Text.Parse; diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index dcbe123e7..ee03643ad 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -7,6 +7,7 @@ using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.Utility; + using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs index 276dddb57..fadf80406 100644 --- a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs +++ b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs @@ -21,7 +21,7 @@ internal class AssertHandler : IDisposable private const int HidePrintEvery = 500; private readonly HashSet ignoredAsserts = []; - private readonly Dictionary assertCounts = new(); + private readonly Dictionary assertCounts = []; // Store callback to avoid it from being GC'd private readonly AssertCallbackDelegate callback; diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index 3e4a5cec6..9812a4e6a 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using CheapLoc; + using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.Command; @@ -11,7 +11,6 @@ using Dalamud.Game.Gui; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Internal; using Dalamud.Utility; -using Serilog; namespace Dalamud.Interface.Internal; @@ -208,7 +207,7 @@ internal class DalamudCommands : IServiceType var chatGui = Service.Get(); var configuration = Service.Get(); - configuration.BadWords ??= new List(); + configuration.BadWords ??= []; if (configuration.BadWords.Count == 0) { @@ -227,7 +226,7 @@ internal class DalamudCommands : IServiceType var chatGui = Service.Get(); var configuration = Service.Get(); - configuration.BadWords ??= new List(); + configuration.BadWords ??= []; configuration.BadWords.RemoveAll(x => x == arguments); @@ -326,7 +325,7 @@ internal class DalamudCommands : IServiceType var configuration = Service.Get(); var localization = Service.Get(); - if (Localization.ApplicableLangCodes.Contains(arguments.ToLowerInvariant()) || arguments.ToLowerInvariant() == "en") + if (Localization.ApplicableLangCodes.Contains(arguments.ToLowerInvariant()) || arguments.Equals("en", StringComparison.InvariantCultureIgnoreCase)) { localization.SetupWithLangCode(arguments.ToLowerInvariant()); configuration.LanguageOverride = arguments.ToLowerInvariant(); diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index cdb976333..e5ff83ff8 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -74,7 +74,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; /// The candidates. - private readonly List<(string String, bool Supported)> candidateStrings = new(); + private readonly List<(string String, bool Supported)> candidateStrings = []; /// The selected imm component. private string compositionString = string.Empty; diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 13e7ff3f7..43f6d6ce7 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImPlot; using Dalamud.Configuration.Internal; @@ -39,9 +40,11 @@ using Dalamud.Plugin.Internal; using Dalamud.Plugin.SelfTest.Internal; using Dalamud.Storage.Assets; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; + using Serilog.Events; namespace Dalamud.Interface.Internal; @@ -54,7 +57,7 @@ internal class DalamudInterface : IInternalDisposableService { private const float CreditsDarkeningMaxAlpha = 0.8f; - private static readonly ModuleLog Log = new("DUI"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly Dalamud dalamud; private readonly DalamudConfiguration configuration; @@ -779,7 +782,7 @@ internal class DalamudInterface : IInternalDisposableService if (ImGui.BeginMenu("Set log level..."u8)) { - foreach (var logLevel in Enum.GetValues(typeof(LogEventLevel)).Cast()) + foreach (var logLevel in Enum.GetValues()) { if (ImGui.MenuItem(logLevel + "##logLevelSwitch", (byte*)null, EntryPoint.LogLevelSwitch.MinimumLevel == logLevel)) { @@ -912,7 +915,7 @@ internal class DalamudInterface : IInternalDisposableService if (ImGui.MenuItem("Cause CLR fastfail"u8)) { - unsafe void CauseFastFail() + static unsafe void CauseFastFail() { // ReSharper disable once NotAccessedVariable var texture = Unsafe.AsRef((void*)0x12345678); diff --git a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs index 33bcffd38..3a69d55ea 100644 --- a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs +++ b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs @@ -1,7 +1,8 @@ -using System.Linq; +using System.Linq; using System.Numerics; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index b7bfd21f1..13623545c 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -3,10 +3,12 @@ using System.Runtime.InteropServices; using System.Text; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui.Toast; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; + using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; @@ -34,7 +36,7 @@ namespace Dalamud.Interface.Internal; [ServiceManager.EarlyLoadedService] internal sealed unsafe class ImGuiClipboardFunctionProvider : IInternalDisposableService { - private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider)); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly void* clipboardUserDataOriginal; private readonly void* setTextOriginal; private readonly void* getTextOriginal; diff --git a/Dalamud/Interface/Internal/ImGuiInputTextStatePtrExtensions.cs b/Dalamud/Interface/Internal/ImGuiInputTextStatePtrExtensions.cs index ab5fdaac8..e27c20d06 100644 --- a/Dalamud/Interface/Internal/ImGuiInputTextStatePtrExtensions.cs +++ b/Dalamud/Interface/Internal/ImGuiInputTextStatePtrExtensions.cs @@ -110,7 +110,7 @@ internal static unsafe class ImGuiInputTextStatePtrExtensions var text = new Span(self.TextW.Data, self.TextW.Size); if (pos != textLen) - text.Slice(pos, textLen - pos).CopyTo(text[(pos + newText.Length)..]); + text[pos..textLen].CopyTo(text[(pos + newText.Length)..]); newText.CopyTo(text[pos..]); self.Edited = true; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 96fcb7dfd..9d75b6aaf 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -9,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -35,7 +35,9 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using Dalamud.Utility.Timing; + using JetBrains.Annotations; + using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -76,10 +78,10 @@ internal partial class InterfaceManager : IInternalDisposableService /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private static readonly ModuleLog Log = new("INTERFACE"); + private static readonly ModuleLog Log = ModuleLog.Create(); - private readonly ConcurrentBag deferredDisposeTextures = new(); - private readonly ConcurrentBag deferredDisposeDisposables = new(); + private readonly ConcurrentBag deferredDisposeTextures = []; + private readonly ConcurrentBag deferredDisposeDisposables = []; [ServiceManager.ServiceDependency] private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); @@ -678,8 +680,7 @@ internal partial class InterfaceManager : IInternalDisposableService if (configuration.SavedStyles == null || configuration.SavedStyles.All(x => x.Name != StyleModelV1.DalamudStandard.Name)) { - configuration.SavedStyles = new List - { StyleModelV1.DalamudStandard, StyleModelV1.DalamudClassic }; + configuration.SavedStyles = [StyleModelV1.DalamudStandard, StyleModelV1.DalamudClassic]; configuration.ChosenStyle = StyleModelV1.DalamudStandard.Name; } else if (configuration.SavedStyles.Count == 1) diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index d3aea7f57..2b7f0f354 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using CheapLoc; + using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; @@ -58,8 +59,8 @@ internal class PluginCategoryManager private CategoryKind currentCategoryKind = CategoryKind.All; private bool isContentDirty; - private Dictionary mapPluginCategories = new(); - private List highlightedCategoryKinds = new(); + private Dictionary mapPluginCategories = []; + private List highlightedCategoryKinds = []; /// /// Type of category group. @@ -513,8 +514,7 @@ internal class PluginCategoryManager this.GroupKind = groupKind; this.nameFunc = nameFunc; - this.Categories = new(); - this.Categories.AddRange(categories); + this.Categories = [.. categories]; } /// diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 82554995b..a79cc1880 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -11,7 +11,6 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.GUI; -using Lumina.Text.ReadOnly; // Customised version of https://github.com/aers/FFXIVUIDebug @@ -25,8 +24,8 @@ internal unsafe class UiDebug private const int UnitListCount = 18; private readonly bool[] selectedInList = new bool[UnitListCount]; - private readonly string[] listNames = new string[UnitListCount] - { + private readonly string[] listNames = + [ "Depth Layer 1", "Depth Layer 2", "Depth Layer 3", @@ -45,7 +44,7 @@ internal unsafe class UiDebug "Units 16", "Units 17", "Units 18", - }; + ]; private bool doingSearch; private string searchInput = string.Empty; @@ -557,7 +556,7 @@ internal unsafe class UiDebug var name = unitBase->NameString; if (searching) { - if (name == null || !name.ToLowerInvariant().Contains(searchStr.ToLowerInvariant())) continue; + if (name == null || !name.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase)) continue; } noResults = false; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs index b31f74264..b5899e15f 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs @@ -2,10 +2,9 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Internal.UiDebug2.Utility; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using Dalamud.Memory; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs index 0b1dcb66c..6ff58d657 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs @@ -41,7 +41,7 @@ public unsafe partial class AddonTree { foreach (var t in from t in ClientStructsAssembly.GetTypes() where t.IsPublic - let xivAddonAttr = (AddonAttribute?)t.GetCustomAttribute(typeof(AddonAttribute), false) + let xivAddonAttr = t.GetCustomAttribute(false) where xivAddonAttr != null where xivAddonAttr.AddonIdentifiers.Contains(this.AddonName) select t) @@ -83,7 +83,7 @@ public unsafe partial class AddonTree foreach (var field in baseType.GetFields(Static | Public | NonPublic | Instance)) { - if (field.GetCustomAttribute(typeof(FieldOffsetAttribute)) is FieldOffsetAttribute offset) + if (field.GetCustomAttribute() is FieldOffsetAttribute offset) { try { diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs index 7cb2cc704..2e0874206 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs @@ -4,7 +4,6 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; -using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs index 98c7d9efe..e094c6d7a 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs @@ -3,6 +3,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Bindings.ImGui.ImGuiTableColumnFlags; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs index 922d226b6..9a51f1771 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs @@ -1,6 +1,7 @@ using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; + using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs index ae6f5fffa..13cad9fd0 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs @@ -5,6 +5,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.UiDebug2.Utility; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Bindings.ImGui.ImGuiColorEditFlags; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs index 260ea4942..aa7ee9a63 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs @@ -2,8 +2,8 @@ using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs index 489135ed0..6844c8a6f 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs @@ -1,6 +1,5 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface.Internal.UiDebug2.Utility; -using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs index 418156811..f38bef400 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs @@ -5,8 +5,8 @@ using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.UiDebug2.Utility; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Bindings.ImGui.ImGuiCol; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs index 1435335db..618517e62 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs @@ -8,6 +8,7 @@ using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.Internal.UiDebug2.Utility; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs index 21f4fb54a..af0e0f284 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs @@ -5,6 +5,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs index 2ea8fd5d2..3286df1b3 100644 --- a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs +++ b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs @@ -8,6 +8,7 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Internal.UiDebug2.Browsing; using Dalamud.Interface.Internal.UiDebug2.Utility; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Component.GUI; using static System.Globalization.NumberFormatInfo; diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs index da4b95256..88af38531 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs @@ -4,6 +4,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface.Internal.UiDebug2.Browsing; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; + using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; diff --git a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs index 210da896f..14da58d94 100644 --- a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs +++ b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs @@ -4,6 +4,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs index ce4c6dfeb..2aaef9256 100644 --- a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs +++ b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; + using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Bindings.ImGui.ImGuiWindowFlags; @@ -21,6 +22,9 @@ namespace Dalamud.Interface.Internal.UiDebug2; /// internal partial class UiDebug2 : IDisposable { + /// + internal static readonly ModuleLog Log = ModuleLog.Create(); + private readonly ElementSelector elementSelector; /// @@ -31,9 +35,6 @@ internal partial class UiDebug2 : IDisposable this.elementSelector = new(this); } - /// - internal static ModuleLog Log { get; set; } = new("UiDebug2"); - /// internal static IGameGui GameGui { get; set; } = Service.Get(); diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs index da5f30e68..5c1e72aed 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs @@ -3,6 +3,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Client.Graphics; using static Dalamud.Bindings.ImGui.ImGuiCol; diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs index 832c7f357..3af306daf 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs @@ -4,6 +4,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; using static System.MathF; diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 44626ba31..c67eebfec 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Numerics; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -22,6 +23,7 @@ using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.UI; namespace Dalamud.Interface.Internal.Windows; @@ -85,7 +87,7 @@ internal sealed class ChangelogWindow : Window, IDisposable private AutoUpdateBehavior? chosenAutoUpdateBehavior; - private Dictionary currentFtueLevels = new(); + private Dictionary currentFtueLevels = []; private DateTime? isEligibleSince; private bool openedThroughEligibility; diff --git a/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs index 1bff1d5c1..564e8ca5e 100644 --- a/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs @@ -5,7 +5,6 @@ using System.Reflection; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; -using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; namespace Dalamud.Interface.Internal.Windows; diff --git a/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs index 9096d78de..61435f723 100644 --- a/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs @@ -19,14 +19,14 @@ internal sealed class ComponentDemoWindow : Window private static readonly TimeSpan DefaultEasingTime = new(0, 0, 0, 1700); private readonly List<(string Name, Action Demo)> componentDemos; - private readonly IReadOnlyList easings = new Easing[] - { + private readonly IReadOnlyList easings = + [ new InSine(DefaultEasingTime), new OutSine(DefaultEasingTime), new InOutSine(DefaultEasingTime), new InCubic(DefaultEasingTime), new OutCubic(DefaultEasingTime), new InOutCubic(DefaultEasingTime), new InQuint(DefaultEasingTime), new OutQuint(DefaultEasingTime), new InOutQuint(DefaultEasingTime), new InCirc(DefaultEasingTime), new OutCirc(DefaultEasingTime), new InOutCirc(DefaultEasingTime), new InElastic(DefaultEasingTime), new OutElastic(DefaultEasingTime), new InOutElastic(DefaultEasingTime), - }; + ]; private int animationTimeMs = (int)DefaultEasingTime.TotalMilliseconds; private Vector4 defaultColor = ImGuiColors.DalamudOrange; @@ -42,14 +42,14 @@ internal sealed class ComponentDemoWindow : Window this.RespectCloseHotkey = false; - this.componentDemos = new() - { + this.componentDemos = + [ ("Test", ImGuiComponents.Test), ("HelpMarker", HelpMarkerDemo), ("IconButton", IconButtonDemo), ("TextWithLabel", TextWithLabelDemo), ("ColorPickerWithPalette", this.ColorPickerWithPaletteDemo), - }; + ]; } /// diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 74df500db..36b0883bb 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; @@ -22,6 +21,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; + using Serilog; using Serilog.Events; @@ -40,7 +40,7 @@ internal class ConsoleWindow : Window, IDisposable private readonly RollingList logText; private readonly RollingList filteredLogEntries; - private readonly List pluginFilters = new(); + private readonly List pluginFilters = []; private readonly DalamudConfiguration configuration; diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index eb0589d59..dbc778614 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -8,6 +8,7 @@ using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.Data; @@ -18,7 +19,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class DataWindow : Window, IDisposable { private readonly IDataWindowWidget[] modules = - { + [ new AddonInspectorWidget(), new AddonInspectorWidget2(), new AddonLifecycleWidget(), @@ -62,7 +63,7 @@ internal class DataWindow : Window, IDisposable new UiColorWidget(), new UldWidget(), new VfsWidget(), - }; + ]; private readonly IOrderedEnumerable orderedModules; diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindowWidgetExtensions.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindowWidgetExtensions.cs index a81d3edf3..d286c4428 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindowWidgetExtensions.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindowWidgetExtensions.cs @@ -3,7 +3,6 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; -using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Data; diff --git a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs index 55f619e9f..2031c66c2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs @@ -5,9 +5,9 @@ using Dalamud.Configuration.Internal; using Dalamud.Game.Inventory; using Dalamud.Game.Inventory.InventoryEventArgTypes; using Dalamud.Interface.Colors; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Logging.Internal; + using Serilog.Events; namespace Dalamud.Interface.Internal.Windows.Data; @@ -17,14 +17,14 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// internal class GameInventoryTestWidget : IDataWindowWidget { - private static readonly ModuleLog Log = new(nameof(GameInventoryTestWidget)); + private static readonly ModuleLog Log = ModuleLog.Create(); private GameInventoryPluginScoped? scoped; private bool standardEnabled; private bool rawEnabled; /// - public string[]? CommandShortcuts { get; init; } = { "gameinventorytest" }; + public string[]? CommandShortcuts { get; init; } = ["gameinventorytest"]; /// public string DisplayName { get; init; } = "GameInventory Test"; diff --git a/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs b/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs index bb8bd8ea1..58cc672e8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs +++ b/Dalamud/Interface/Internal/Windows/Data/WidgetUtil.cs @@ -1,5 +1,4 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Data; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs index e11404dec..c8a747239 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs @@ -8,7 +8,7 @@ internal class AddonInspectorWidget : IDataWindowWidget private UiDebug? addonInspector; /// - public string[]? CommandShortcuts { get; init; } = { "ai", "addoninspector" }; + public string[]? CommandShortcuts { get; init; } = ["ai", "addoninspector"]; /// public string DisplayName { get; init; } = "Addon Inspector"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 4fb13b81a..0316cc84e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -19,7 +19,7 @@ public class AddonLifecycleWidget : IDataWindowWidget public string DisplayName { get; init; } = "Addon Lifecycle"; /// - [MemberNotNullWhen(true, "AddonLifecycle")] + [MemberNotNullWhen(true, nameof(AddonLifecycle))] public bool Ready { get; set; } private AddonLifecycle? AddonLifecycle { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs index 6f41fa46f..06c7ea393 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Bindings.ImGui; using Dalamud.Game; @@ -15,7 +15,7 @@ internal class AddressesWidget : IDataWindowWidget private nint sigResult = nint.Zero; /// - public string[]? CommandShortcuts { get; init; } = { "address" }; + public string[]? CommandShortcuts { get; init; } = ["address"]; /// public string DisplayName { get; init; } = "Addresses"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs index cc5492228..f414a9423 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs @@ -12,7 +12,7 @@ internal class AetherytesWidget : IDataWindowWidget public bool Ready { get; set; } /// - public string[]? CommandShortcuts { get; init; } = { "aetherytes" }; + public string[]? CommandShortcuts { get; init; } = ["aetherytes"]; /// public string DisplayName { get; init; } = "Aetherytes"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs index c3074e807..03f5ab32e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs @@ -3,8 +3,10 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; + using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; + using Lumina.Text.ReadOnly; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -32,7 +34,7 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget public bool Ready { get; set; } /// - public string[]? CommandShortcuts { get; init; } = { "atkarray" }; + public string[]? CommandShortcuts { get; init; } = ["atkarray"]; /// public string DisplayName { get; init; } = "Atk Array Data"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs index 06dc1b11e..10efdbae1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs @@ -15,7 +15,7 @@ internal class BuddyListWidget : IDataWindowWidget public bool Ready { get; set; } /// - public string[]? CommandShortcuts { get; init; } = { "buddy", "buddylist" }; + public string[]? CommandShortcuts { get; init; } = ["buddy", "buddylist"]; /// public string DisplayName { get; init; } = "Buddy List"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs index f5a521672..1082bf6ca 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs @@ -2,7 +2,6 @@ using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Command; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -13,7 +12,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class CommandWidget : IDataWindowWidget { /// - public string[]? CommandShortcuts { get; init; } = { "command" }; + public string[]? CommandShortcuts { get; init; } = ["command"]; /// public string DisplayName { get; init; } = "Command"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs index 6ecee48ed..e3a737a25 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Conditions; using Dalamud.Utility; @@ -13,7 +13,7 @@ internal class ConditionWidget : IDataWindowWidget public bool Ready { get; set; } /// - public string[]? CommandShortcuts { get; init; } = { "condition" }; + public string[]? CommandShortcuts { get; init; } = ["condition"]; /// public string DisplayName { get; init; } = "Condition"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs index f66b50fca..c6a3477ae 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs @@ -1,4 +1,4 @@ -using Dalamud.Configuration.Internal; +using Dalamud.Configuration.Internal; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -9,7 +9,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class ConfigurationWidget : IDataWindowWidget { /// - public string[]? CommandShortcuts { get; init; } = { "config", "configuration" }; + public string[]? CommandShortcuts { get; init; } = ["config", "configuration"]; /// public string DisplayName { get; init; } = "Configuration"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 83db4ac6e..8c9774840 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Ipc.Internal; + using Newtonsoft.Json; using Formatting = Newtonsoft.Json.Formatting; @@ -28,13 +29,13 @@ internal class DataShareWidget : IDataWindowWidget { private const ImGuiTabItemFlags NoCloseButton = (ImGuiTabItemFlags)(1 << 20); - private readonly List<(string Name, byte[]? Data)> dataView = new(); + private readonly List<(string Name, byte[]? Data)> dataView = []; private int nextTab = -1; private IReadOnlyDictionary? gates; private List? gatesSorted; /// - public string[]? CommandShortcuts { get; init; } = { "datashare" }; + public string[]? CommandShortcuts { get; init; } = ["datashare"]; /// public string DisplayName { get; init; } = "Data Share & Call Gate"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs index 0a40c9be7..838f11632 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Threading; using Dalamud.Bindings.ImGui; @@ -21,7 +21,7 @@ internal class DtrBarWidget : IDataWindowWidget, IDisposable private CancellationTokenSource? loadTestThreadCt; /// - public string[]? CommandShortcuts { get; init; } = { "dtr", "dtrbar" }; + public string[]? CommandShortcuts { get; init; } = ["dtr", "dtrbar"]; /// public string DisplayName { get; init; } = "DTR Bar"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs index 50ed79b3d..e241f157d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs @@ -1,7 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Fates; using Dalamud.Interface.Textures.Internal; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -12,7 +11,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class FateTableWidget : IDataWindowWidget { /// - public string[]? CommandShortcuts { get; init; } = { "fate", "fatetable" }; + public string[]? CommandShortcuts { get; init; } = ["fate", "fatetable"]; /// public string DisplayName { get; init; } = "Fate Table"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs index d20aa5cb1..7910daaec 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs @@ -22,7 +22,7 @@ internal class FlyTextWidget : IDataWindowWidget private Vector4 flyColor = new(1, 0, 0, 1); /// - public string[]? CommandShortcuts { get; init; } = { "flytext" }; + public string[]? CommandShortcuts { get; init; } = ["flytext"]; /// public string DisplayName { get; init; } = "Fly Text"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index ea4b80247..8beb437ac 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -27,7 +27,7 @@ internal class FontAwesomeTestWidget : IDataWindowWidget private bool useFixedWidth = false; /// - public string[]? CommandShortcuts { get; init; } = { "fa", "fatest", "fontawesome" }; + public string[]? CommandShortcuts { get; init; } = ["fa", "fatest", "fontawesome"]; /// public string DisplayName { get; init; } = "Font Awesome Test"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index c653e9185..32ad076db 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; @@ -15,6 +14,7 @@ using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -25,11 +25,11 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { private static readonly string[] FontScaleModes = - { + [ nameof(FontScaleMode.Default), nameof(FontScaleMode.SkipHandling), nameof(FontScaleMode.UndoGlobalScale), - }; + ]; private ImVectorWrapper testStringBuffer; private IFontAtlas? privateAtlas; @@ -338,7 +338,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable return; - void TestSingle(ImFontPtr fontPtr, IFontHandle handle) + static void TestSingle(ImFontPtr fontPtr, IFontHandle handle) { var dim = ImGui.CalcTextSizeA(fontPtr, fontPtr.FontSize, float.MaxValue, 0f, "Test string"u8, out _); Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {handle} => {dim}"); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs index 65e6cd3d6..ed1a4da5b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.GamePad; using Dalamud.Utility; @@ -10,7 +10,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class GamepadWidget : IDataWindowWidget { /// - public string[]? CommandShortcuts { get; init; } = { "gamepad", "controller" }; + public string[]? CommandShortcuts { get; init; } = ["gamepad", "controller"]; /// public string DisplayName { get; init; } = "Gamepad"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs index 7a5a9c89b..09bd29851 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs @@ -12,7 +12,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class GaugeWidget : IDataWindowWidget { /// - public string[]? CommandShortcuts { get; init; } = { "gauge", "jobgauge", "job" }; + public string[]? CommandShortcuts { get; init; } = ["gauge", "jobgauge", "job"]; /// public string DisplayName { get; init; } = "Job Gauge"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index f3e25caf8..ad06e12fd 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -6,8 +6,11 @@ using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Game; using Dalamud.Hooking; + using FFXIVClientStructs.FFXIV.Component.GUI; + using Serilog; + using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index 0395f4c96..77ca7ec2b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -33,7 +33,7 @@ public class IconBrowserWidget : IDataWindowWidget private Vector2 lastWindowSize = Vector2.Zero; /// - public string[]? CommandShortcuts { get; init; } = { "icon", "icons" }; + public string[]? CommandShortcuts { get; init; } = ["icon", "icons"]; /// public string DisplayName { get; init; } = "Icon Browser"; @@ -269,7 +269,7 @@ public class IconBrowserWidget : IDataWindowWidget if (this.valueRange is not null) return; - this.valueRange = new(); + this.valueRange = []; foreach (var (id, _) in this.iconIdsTask!.Result) { if (this.startRange <= id && id < this.stopRange) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index d4afce48d..4327fa12f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -19,11 +19,11 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class ImGuiWidget : IDataWindowWidget { - private readonly HashSet notifications = new(); + private readonly HashSet notifications = []; private NotificationTemplate notificationTemplate; /// - public string[]? CommandShortcuts { get; init; } = { "imgui" }; + public string[]? CommandShortcuts { get; init; } = ["imgui"]; /// public string DisplayName { get; init; } = "ImGui"; @@ -334,7 +334,7 @@ internal class ImGuiWidget : IDataWindowWidget private struct NotificationTemplate { public static readonly string[] IconTitles = - { + [ "None (use Type)", "SeIconChar", "FontAwesomeIcon", @@ -344,7 +344,7 @@ internal class ImGuiWidget : IDataWindowWidget "TextureWrap from DalamudAssets(Async)", "TextureWrap from GamePath", "TextureWrap from FilePath", - }; + ]; public static readonly string[] AssetSources = Enum.GetValues() @@ -353,46 +353,46 @@ internal class ImGuiWidget : IDataWindowWidget .ToArray(); public static readonly string[] ProgressModeTitles = - { + [ "Default", "Random", "Increasing", "Increasing & Auto Dismiss", "Indeterminate", - }; + ]; public static readonly string[] TypeTitles = - { + [ nameof(NotificationType.None), nameof(NotificationType.Success), nameof(NotificationType.Warning), nameof(NotificationType.Error), nameof(NotificationType.Info), - }; + ]; public static readonly string[] InitialDurationTitles = - { + [ "Infinite", "1 seconds", "3 seconds (default)", "10 seconds", - }; + ]; public static readonly string[] HoverExtendDurationTitles = - { + [ "Disable", "1 seconds", "3 seconds (default)", "10 seconds", - }; + ]; public static readonly TimeSpan[] Durations = - { + [ TimeSpan.Zero, TimeSpan.FromSeconds(1), NotificationConstants.DefaultDuration, TimeSpan.FromSeconds(10), - }; + ]; public bool ManualContent; public string Content; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs index c0ec3d490..f5b26c04c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs @@ -10,7 +10,9 @@ using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.Game; + using Lumina.Excel.Sheets; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs index fa615ed47..1b45b58fe 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Colors; @@ -10,7 +10,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class KeyStateWidget : IDataWindowWidget { /// - public string[]? CommandShortcuts { get; init; } = { "keystate" }; + public string[]? CommandShortcuts { get; init; } = ["keystate"]; /// public string DisplayName { get; init; } = "KeyState"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs index 56de19de7..382c42f91 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs @@ -1,10 +1,9 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Globalization; using Dalamud.Bindings.ImGui; using Dalamud.Game.MarketBoard; using Dalamud.Game.Network.Structures; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using ImGuiTable = Dalamud.Interface.Utility.ImGuiTable; @@ -44,7 +43,7 @@ internal class MarketBoardWidget : IDataWindowWidget } /// - public string[]? CommandShortcuts { get; init; } = { "marketboard" }; + public string[]? CommandShortcuts { get; init; } = ["marketboard"]; /// public string DisplayName { get; init; } = "Market Board"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 4a32a16df..7761a18b4 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -42,7 +42,7 @@ internal class NetworkMonitorWidget : IDataWindowWidget } /// - public string[]? CommandShortcuts { get; init; } = { "network", "netmon", "networkmonitor" }; + public string[]? CommandShortcuts { get; init; } = ["network", "netmon", "networkmonitor"]; /// public string DisplayName { get; init; } = "Network Monitor"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs index cbf5c3355..eac8b5a52 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs @@ -8,6 +8,7 @@ using Dalamud.Game.ClientState; using Dalamud.Game.Text.Noun; using Dalamud.Game.Text.Noun.Enums; using Dalamud.Interface.Utility.Raii; + using Lumina.Data; using Lumina.Excel; using Lumina.Excel.Sheets; @@ -60,7 +61,7 @@ internal class NounProcessorWidget : IDataWindowWidget private int amount = 1; /// - public string[]? CommandShortcuts { get; init; } = { "noun" }; + public string[]? CommandShortcuts { get; init; } = ["noun"]; /// public string DisplayName { get; init; } = "Noun Processor"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index 71fb18352..dd5dc7472 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -19,7 +19,7 @@ internal class ObjectTableWidget : IDataWindowWidget private float maxCharaDrawDistance = 20.0f; /// - public string[]? CommandShortcuts { get; init; } = { "ot", "objecttable" }; + public string[]? CommandShortcuts { get; init; } = ["ot", "objecttable"]; /// public string DisplayName { get; init; } = "Object Table"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs index e43b231be..4033e4f41 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Party; using Dalamud.Utility; @@ -12,7 +12,7 @@ internal class PartyListWidget : IDataWindowWidget private bool resolveGameData; /// - public string[]? CommandShortcuts { get; init; } = { "partylist", "party" }; + public string[]? CommandShortcuts { get; init; } = ["partylist", "party"]; /// public string DisplayName { get; init; } = "Party List"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs index 1f1f82cdd..72a02b219 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs @@ -25,7 +25,7 @@ internal class PluginIpcWidget : IDataWindowWidget private string callGateResponse = string.Empty; /// - public string[]? CommandShortcuts { get; init; } = { "ipc" }; + public string[]? CommandShortcuts { get; init; } = ["ipc"]; /// public string DisplayName { get; init; } = "Plugin IPC"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs index 17b7959f6..87c0afe58 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; @@ -11,7 +11,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class SeFontTestWidget : IDataWindowWidget { /// - public string[]? CommandShortcuts { get; init; } = { "sefont", "sefonttest" }; + public string[]? CommandShortcuts { get; init; } = ["sefont", "sefonttest"]; /// public string DisplayName { get; init; } = "SeFont Test"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs index 6a07152e5..8f5fe7b8a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs @@ -13,7 +13,9 @@ 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; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs index 78ea6d233..3ddc2a888 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Reflection; @@ -16,15 +16,15 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class ServicesWidget : IDataWindowWidget { - private readonly Dictionary nodeRects = new(); - private readonly HashSet selectedNodes = new(); - private readonly HashSet tempRelatedNodes = new(); + private readonly Dictionary nodeRects = []; + private readonly HashSet selectedNodes = []; + private readonly HashSet tempRelatedNodes = []; private bool includeUnloadDependencies; private List>? dependencyNodes; /// - public string[]? CommandShortcuts { get; init; } = { "services" }; + public string[]? CommandShortcuts { get; init; } = ["services"]; /// public string DisplayName { get; init; } = "Service Container"; @@ -280,9 +280,9 @@ internal class ServicesWidget : IDataWindowWidget private class ServiceDependencyNode { - private readonly List parents = new(); - private readonly List children = new(); - private readonly List invalidParents = new(); + private readonly List parents = []; + private readonly List children = []; + private readonly List invalidParents = []; private ServiceDependencyNode(Type t) { @@ -370,7 +370,7 @@ internal class ServicesWidget : IDataWindowWidget foreach (var n in CreateTree(includeUnloadDependencies)) { while (res.Count <= n.Level) - res.Add(new()); + res.Add([]); res[n.Level].Add(n); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs index 7fb2cc2bf..c0c38da24 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs @@ -1,4 +1,5 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; + using Newtonsoft.Json; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -9,7 +10,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class StartInfoWidget : IDataWindowWidget { /// - public string[]? CommandShortcuts { get; init; } = { "startinfo" }; + public string[]? CommandShortcuts { get; init; } = ["startinfo"]; /// public string DisplayName { get; init; } = "Start Info"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index 6caf3286d..2e52d7586 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -14,7 +14,7 @@ internal class TargetWidget : IDataWindowWidget private bool resolveGameData; /// - public string[]? CommandShortcuts { get; init; } = { "target" }; + public string[]? CommandShortcuts { get; init; } = ["target"]; /// public string DisplayName { get; init; } = "Target"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index cd72d751e..d9cf0fea2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -17,6 +16,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Logging.Internal; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -35,7 +35,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget private CancellationTokenSource taskSchedulerCancelSource = new(); /// - public string[]? CommandShortcuts { get; init; } = { "tasksched", "taskscheduler" }; + public string[]? CommandShortcuts { get; init; } = ["tasksched", "taskscheduler"]; /// public string DisplayName { get; init; } = "Task Scheduler"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 3416a2506..e6a092b6e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -17,6 +17,7 @@ using Dalamud.Interface.Utility.Internal; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; + using TerraFX.Interop.DirectX; using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager; @@ -43,7 +44,7 @@ internal class TexWidget : IDataWindowWidget [DrawBlameTableColumnUserId.NativeAddress] = static x => x.ResourceAddress, }; - private readonly List addedTextures = new(); + private readonly List addedTextures = []; private string allLoadedTexturesTableName = "##table"; private string iconId = "18"; @@ -83,7 +84,7 @@ internal class TexWidget : IDataWindowWidget } /// - public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; + public string[]? CommandShortcuts { get; init; } = ["tex", "texture"]; /// public string DisplayName { get; init; } = "Tex"; @@ -137,9 +138,9 @@ internal class TexWidget : IDataWindowWidget conf.QueueSave(); } - var allBlames = this.textureManager.BlameTracker; - lock (allBlames) + lock (this.textureManager.BlameTracker) { + var allBlames = this.textureManager.BlameTracker; ImGui.PushID("blames"u8); var sizeSum = allBlames.Sum(static x => Math.Max(0, x.RawSpecs.EstimatedBytes)); if (ImGui.CollapsingHeader( @@ -609,7 +610,7 @@ internal class TexWidget : IDataWindowWidget ImGui.SameLine(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Sync)) - this.textureManager.InvalidatePaths(new[] { texture.SourcePathForDebug }); + this.textureManager.InvalidatePaths([texture.SourcePathForDebug]); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Call {nameof(ITextureSubstitutionProvider.InvalidatePaths)}."); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs index 5e5a077c3..6be0a3a85 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs @@ -20,7 +20,7 @@ internal class ToastWidget : IDataWindowWidget private bool questToastCheckmark; /// - public string[]? CommandShortcuts { get; init; } = { "toast" }; + public string[]? CommandShortcuts { get; init; } = ["toast"]; /// public string DisplayName { get; init; } = "Toast"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index fd3f1d11c..029dc0b75 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -7,6 +7,7 @@ using Dalamud.Data; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.ImGuiSeStringRenderer.Internal; + using Lumina.Excel.Sheets; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index 7c8110301..cc291af1a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -13,6 +13,7 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Utility; using Dalamud.Memory; + using Lumina.Data.Files; using Lumina.Data.Parsing.Uld; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs index f044b2989..d01bd7d78 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs @@ -4,6 +4,7 @@ using System.IO; using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Storage; + using Serilog; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -17,7 +18,7 @@ internal class VfsWidget : IDataWindowWidget private int reps = 1; /// - public string[]? CommandShortcuts { get; init; } = { "vfs" }; + public string[]? CommandShortcuts { get; init; } = ["vfs"]; /// public string DisplayName { get; init; } = "VFS Performance"; diff --git a/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs index 91f7f02d9..edad04951 100644 --- a/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs +++ b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs @@ -1,6 +1,7 @@ using System.Numerics; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index e95d2e1b8..fb77cf1cf 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -6,7 +6,6 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -using Dalamud.Game; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Networking.Http; @@ -15,6 +14,7 @@ using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Storage.Assets; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows; @@ -51,8 +51,8 @@ internal class PluginImageCache : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly HappyHttpClient happyHttpClient = Service.Get(); - private readonly BlockingCollection>> downloadQueue = new(); - private readonly BlockingCollection> loadQueue = new(); + private readonly BlockingCollection>> downloadQueue = []; + private readonly BlockingCollection> loadQueue = []; private readonly CancellationTokenSource cancelToken = new(); private readonly Task downloadTask; private readonly Task loadTask; @@ -144,7 +144,7 @@ internal class PluginImageCache : IInternalDisposableService this.downloadQueue.CompleteAdding(); this.loadQueue.CompleteAdding(); - if (!Task.WaitAll(new[] { this.loadTask, this.downloadTask }, 4000)) + if (!Task.WaitAll([this.loadTask, this.downloadTask], 4000)) { Log.Error("Plugin Image download/load thread has not cancelled in time"); } @@ -357,7 +357,7 @@ internal class PluginImageCache : IInternalDisposableService try { token.ThrowIfCancellationRequested(); - if (!pendingFuncs.Any()) + if (pendingFuncs.Count == 0) { if (!this.downloadQueue.TryTake(out var taskTuple, -1, token)) return; @@ -373,7 +373,7 @@ internal class PluginImageCache : IInternalDisposableService pendingFuncs = pendingFuncs.OrderBy(x => x.Item1).ToList(); var item1 = pendingFuncs.Last().Item1; - while (pendingFuncs.Any() && pendingFuncs.Last().Item1 == item1) + while (pendingFuncs.Count != 0 && pendingFuncs.Last().Item1 == item1) { token.ThrowIfCancellationRequested(); while (runningTasks.Count >= concurrency) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs index f6171e192..bbc92efb5 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Net.Http.Json; using System.Threading.Tasks; @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.PluginInstaller; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs index 879034fd4..3f964b4b8 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs @@ -1,5 +1,7 @@ -using CheapLoc; +using CheapLoc; + using Dalamud.Plugin.Internal.Types; + using Serilog; namespace Dalamud.Interface.Internal.Windows.PluginInstaller; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 3241015fc..e32d31181 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Console; @@ -41,7 +42,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller; /// internal class PluginInstallerWindow : Window, IDisposable { - private static readonly ModuleLog Log = new("PLUGINW"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly Vector4 changelogBgColor = new(0.114f, 0.584f, 0.192f, 0.678f); private readonly Vector4 changelogTextColor = new(0.812f, 1.000f, 0.816f, 1.000f); @@ -49,7 +50,7 @@ internal class PluginInstallerWindow : Window, IDisposable private readonly PluginImageCache imageCache; private readonly PluginCategoryManager categoryManager = new(); - private readonly List openPluginCollapsibles = new(); + private readonly List openPluginCollapsibles = []; private readonly DateTime timeLoaded; @@ -113,9 +114,9 @@ internal class PluginInstallerWindow : Window, IDisposable private List? updatedPlugins; [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Makes sense like this")] - private List pluginListAvailable = new(); - private List pluginListInstalled = new(); - private List pluginListUpdatable = new(); + private List pluginListAvailable = []; + private List pluginListInstalled = []; + private List pluginListUpdatable = []; private bool hasDevPlugins = false; private bool hasHiddenPlugins = false; @@ -2360,7 +2361,7 @@ internal class PluginInstallerWindow : Window, IDisposable else if (!string.IsNullOrWhiteSpace(manifest.Description)) { const int punchlineLen = 200; - var firstLine = manifest.Description.Split(new[] { '\r', '\n' })[0]; + var firstLine = manifest.Description.Split(['\r', '\n'])[0]; ImGui.TextWrapped(firstLine.Length < punchlineLen ? firstLine diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index fb2719868..c01d7f390 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; @@ -15,6 +16,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -403,7 +405,7 @@ internal class ProfileManagerWidget ImGui.Text(Locs.StartupBehavior); if (ImGui.BeginCombo("##startupBehaviorPicker"u8, Locs.PolicyToLocalisedName(profile.StartupPolicy))) { - foreach (var policy in Enum.GetValues(typeof(ProfileModelV1.ProfileStartupPolicy)).Cast()) + foreach (var policy in Enum.GetValues()) { var name = Locs.PolicyToLocalisedName(policy); if (ImGui.Selectable(name, profile.StartupPolicy == policy)) diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index b8e10020e..8b702123c 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Reflection; @@ -14,6 +13,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows; @@ -186,7 +186,7 @@ internal class PluginStatWindow : Window ImGui.SameLine(); ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage:F4}ms", "All average update times added together"); ImGui.SameLine(); - ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times"); + ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Length != 0 ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times"); ImGui.InputTextWithHint( "###PluginStatWindow_FrameworkSearch"u8, @@ -230,7 +230,7 @@ internal class PluginStatWindow : Window foreach (var handlerHistory in statsHistory) { - if (!handlerHistory.Value.Any()) + if (handlerHistory.Value.Count == 0) { continue; } diff --git a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs index abe8d1584..8ff407cd7 100644 --- a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs @@ -19,7 +19,7 @@ public class ProfilerWindow : Window { private double min; private double max; - private List>> occupied = new(); + private List>> occupied = []; /// /// Initializes a new instance of the class. @@ -109,7 +109,7 @@ public class ProfilerWindow : Window } if (depth == this.occupied.Count) - this.occupied.Add(new()); + this.occupied.Add([]); this.occupied[depth].Add(Tuple.Create(timingHandle.StartTime, timingHandle.EndTime)); parentDepthDict[timingHandle.Id] = depth; @@ -188,7 +188,7 @@ public class ProfilerWindow : Window } } - uint eventTextDepth = maxRectDept + 2; + var eventTextDepth = maxRectDept + 2; var eventsXPos = new List(); const float eventsXPosFudge = 5f; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index ea8cd0070..0335cafc5 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -10,6 +10,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; + using Dalamud.Plugin.SelfTest; using Dalamud.Plugin.SelfTest.Internal; using Dalamud.Utility; @@ -21,11 +22,11 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest; /// internal class SelfTestWindow : Window { - private static readonly ModuleLog Log = new("AGING"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly SelfTestRegistry selfTestRegistry; - private List visibleSteps = new(); + private List visibleSteps = []; private bool selfTestRunning = false; private SelfTestGroup? currentTestGroup = null; @@ -138,7 +139,7 @@ internal class SelfTestWindow : Window ImGui.SameLine(); var stepNumber = this.currentStep != null ? this.visibleSteps.IndexOf(this.currentStep) : 0; - ImGui.Text($"Step: {stepNumber} / {this.visibleSteps.Count}"); + ImGui.Text($"Step: {stepNumber} / {this.visibleSteps.Count}"); ImGui.Spacing(); diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AddonLifecycleSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AddonLifecycleSelfTestStep.cs index d9c9facc7..e2c1a40df 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AddonLifecycleSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/AddonLifecycleSelfTestStep.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; @@ -23,15 +23,15 @@ internal class AddonLifecycleSelfTestStep : ISelfTestStep /// public AddonLifecycleSelfTestStep() { - this.listeners = new List - { + this.listeners = + [ new(AddonEvent.PostSetup, "Character", this.PostSetup), new(AddonEvent.PostUpdate, "Character", this.PostUpdate), new(AddonEvent.PostDraw, "Character", this.PostDraw), new(AddonEvent.PostRefresh, "Character", this.PostRefresh), new(AddonEvent.PostRequestedUpdate, "Character", this.PostRequestedUpdate), new(AddonEvent.PreFinalize, "Character", this.PreFinalize), - }; + ]; } private enum TestStep diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs index a34b058bd..1f33e5dd2 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs @@ -1,6 +1,5 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Command; -using Dalamud.Interface.Utility; using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ConditionSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ConditionSelfTestStep.cs index 89083da48..1c9b589d0 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ConditionSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ConditionSelfTestStep.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.SelfTest; + using Serilog; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs index 0fe5b4443..b61c62589 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs @@ -9,8 +9,10 @@ using Dalamud.Game.Gui.ContextMenu; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.SelfTest; + using Lumina.Excel; using Lumina.Excel.Sheets; + using Serilog; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FrameworkTaskSchedulerSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FrameworkTaskSchedulerSelfTestStep.cs index eb6909fa7..c5f3ab76b 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FrameworkTaskSchedulerSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/FrameworkTaskSchedulerSelfTestStep.cs @@ -4,8 +4,6 @@ using Dalamud.Game; using Dalamud.Plugin.SelfTest; using Dalamud.Utility; -using Microsoft.VisualBasic.Logging; - using Log = Serilog.Log; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs index 741dd71b1..dd8a16689 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs @@ -1,6 +1,7 @@ using Dalamud.Data; using Dalamud.Plugin.SelfTest; using Dalamud.Utility; + using Lumina.Excel; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs index 6a45f343a..ff6b64383 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs @@ -4,7 +4,6 @@ using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.MarketBoard; using Dalamud.Game.Network.Structures; -using Dalamud.Interface.Utility; using Dalamud.Plugin.SelfTest; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NamePlateSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NamePlateSelfTestStep.cs index 9cc6045a6..7136c8801 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NamePlateSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/NamePlateSelfTestStep.cs @@ -36,7 +36,7 @@ internal class NamePlateSelfTestStep : ISelfTestStep namePlateGui.OnNamePlateUpdate += this.OnNamePlateUpdate; namePlateGui.OnDataUpdate += this.OnDataUpdate; namePlateGui.RequestRedraw(); - this.updateCount = new Dictionary(); + this.updateCount = []; this.currentSubStep++; break; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs index c285fda46..c99ec91de 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs @@ -4,6 +4,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game; using Dalamud.Game.Text.Evaluator.Internal; using Dalamud.Plugin.SelfTest; + using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI.Misc; diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 62c931b20..aa8d1dc3a 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Numerics; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 4785ceb3c..1d5eebb85 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; @@ -15,6 +16,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using Dalamud.Storage.Assets; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.Game.UI; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs index 6b99c5c24..b03a0f51c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs index 4b055b35b..18382b1f8 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Game.Gui.Dtr; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 9b2c418b6..f2ca1f53a 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Text; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Game; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs index 8fb91940e..096e408b8 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs @@ -2,8 +2,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using CheapLoc; - using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs index 5737b44db..daa91420f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration; using Dalamud.Configuration.Internal; diff --git a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs index f9e8022a1..4add874ba 100644 --- a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs +++ b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Reflection; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; @@ -12,6 +12,7 @@ using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.StyleEditor; @@ -50,7 +51,7 @@ public class StyleEditorWindow : Window this.didSave = false; var config = Service.Get(); - config.SavedStyles ??= new List(); + config.SavedStyles ??= []; this.currentSel = config.SavedStyles.FindIndex(x => x.Name == config.ChosenStyle); this.initialStyle = config.ChosenStyle; diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index ec9440e0e..9d4f7ab04 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -22,8 +22,11 @@ using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; + using Lumina.Text.ReadOnly; + using Serilog; namespace Dalamud.Interface.Internal.Windows; @@ -47,9 +50,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly Lazy shadeTexture; private readonly AddonLifecycleEventListener versionStringListener; - private readonly Dictionary shadeEasings = new(); - private readonly Dictionary moveEasings = new(); - private readonly Dictionary logoEasings = new(); + private readonly Dictionary shadeEasings = []; + private readonly Dictionary moveEasings = []; + private readonly Dictionary logoEasings = []; private readonly IConsoleVariable showTsm; diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs index 2e497b6cd..b2b2c2ab1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using System.Text.Unicode; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Utility; namespace Dalamud.Interface.ManagedFontAtlas; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index f2c91d264..0e2f503b4 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; @@ -34,8 +35,8 @@ internal sealed class DelegateFontHandle : FontHandle /// internal sealed class HandleManager : IFontHandleManager { - private readonly HashSet handles = new(); - private readonly object syncRoot = new(); + private readonly HashSet handles = []; + private readonly Lock syncRoot = new(); /// /// Initializes a new instance of the class. @@ -95,8 +96,8 @@ internal sealed class DelegateFontHandle : FontHandle private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); // Owned by this class, but ImFontPtr values still do not belong to this. - private readonly Dictionary fonts = new(); - private readonly Dictionary buildExceptions = new(); + private readonly Dictionary fonts = []; + private readonly Dictionary buildExceptions = []; /// /// Initializes a new instance of the class. diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 41c87fd39..97dc29804 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -15,6 +15,7 @@ using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Storage.Assets; using Dalamud.Utility; + using TerraFX.Interop.DirectX; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -24,8 +25,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// internal sealed partial class FontAtlasFactory { - private static readonly Dictionary> PairAdjustmentsCache = - new(); + private static readonly Dictionary> PairAdjustmentsCache = []; /// /// Implementations for and @@ -43,7 +43,7 @@ internal sealed partial class FontAtlasFactory private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance; private readonly FontAtlasFactory factory; private readonly FontAtlasBuiltData data; - private readonly List registeredPostBuildActions = new(); + private readonly List registeredPostBuildActions = []; /// /// Initializes a new instance of the class. @@ -85,7 +85,7 @@ internal sealed partial class FontAtlasFactory /// /// Gets the font scale modes. /// - private Dictionary FontScaleModes { get; } = new(); + private Dictionary FontScaleModes { get; } = []; /// public void Dispose() => this.disposeAfterBuild.Dispose(); @@ -170,7 +170,7 @@ internal sealed partial class FontAtlasFactory }; if (fontConfig.GlyphRanges is not { Length: > 0 } ranges) - ranges = new ushort[] { 1, 0xFFFE, 0 }; + ranges = [1, 0xFFFE, 0]; raw.GlyphRanges = (ushort*)this.DisposeAfterBuild( GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject(); @@ -188,7 +188,7 @@ internal sealed partial class FontAtlasFactory { if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments)) { - PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new()); + PairAdjustmentsCache.Add(hashIdent, pairAdjustments = []); try { pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray()); @@ -382,7 +382,7 @@ internal sealed partial class FontAtlasFactory DalamudAsset.FontAwesomeFreeSolid, fontConfig with { - GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 }, + GlyphRanges = [FontAwesomeIconMin, FontAwesomeIconMax, 0], }); /// @@ -391,12 +391,12 @@ internal sealed partial class FontAtlasFactory DalamudAsset.LodestoneGameSymbol, fontConfig with { - GlyphRanges = new ushort[] - { + GlyphRanges = + [ GamePrebakedFontHandle.SeIconCharMin, GamePrebakedFontHandle.SeIconCharMax, 0, - }, + ], }); /// @@ -629,7 +629,7 @@ internal sealed partial class FontAtlasFactory { this.AddDalamudAssetFont( DalamudAsset.NotoSansJpMedium, - new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 }); + new() { GlyphRanges = [' ', ' ', '\0'], SizePx = 1 }); } if (!this.NewImAtlas.Build()) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 430f26127..323d4173d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -1,4 +1,4 @@ -// #define VeryVerboseLog +// #define VeryVerboseLog using System.Collections.Generic; using System.Diagnostics; @@ -41,9 +41,9 @@ internal sealed partial class FontAtlasFactory /// /// If set, disables concurrent font build operation. /// - private static readonly object? NoConcurrentBuildOperationLock = null; // new(); + private static readonly Lock? NoConcurrentBuildOperationLock = null; // new(); - private static readonly ModuleLog Log = new(nameof(FontAtlasFactory)); + private static readonly ModuleLog Log = ModuleLog.Create(); private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); @@ -66,10 +66,10 @@ internal sealed partial class FontAtlasFactory try { - var substancesList = this.substances = new(); + var substancesList = this.substances = []; this.Garbage.Add(() => substancesList.Clear()); - var wrapsCopy = this.wraps = new(); + var wrapsCopy = this.wraps = []; this.Garbage.Add(() => wrapsCopy.Clear()); var atlasPtr = ImGui.ImFontAtlas(); @@ -115,16 +115,14 @@ internal sealed partial class FontAtlasFactory public void AddExistingTexture(IDalamudTextureWrap wrap) { - if (this.wraps is null) - throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + ObjectDisposedException.ThrowIf(this.wraps == null, this); this.wraps.Add(this.Garbage.Add(wrap)); } public int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) { - if (this.wraps is null) - throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + ObjectDisposedException.ThrowIf(this.wraps == null, this); var handle = wrap.Handle; var index = this.ImTextures.IndexOf(x => x.TexID == handle); @@ -254,7 +252,7 @@ internal sealed partial class FontAtlasFactory private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; private readonly IFontHandleManager[] fontHandleManagers; - private readonly object syncRoot = new(); + private readonly Lock syncRoot = new(); private Task buildTask = EmptyTask; private FontAtlasBuiltData? builtData; @@ -292,13 +290,13 @@ internal sealed partial class FontAtlasFactory this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend; this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend); - this.fontHandleManagers = new IFontHandleManager[] - { + this.fontHandleManagers = + [ this.delegateFontHandleManager = this.disposables.Add( new DelegateFontHandle.HandleManager(atlasName)), this.gameFontHandleManager = this.disposables.Add( new GamePrebakedFontHandle.HandleManager(atlasName, factory)), - }; + ]; foreach (var fhm in this.fontHandleManagers) fhm.RebuildRecommend += this.OnRebuildRecommend; } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 6ae810dec..55c2acdbc 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -18,7 +18,9 @@ using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; using Dalamud.Utility; + using Lumina.Data.Files; + using TerraFX.Interop.DirectX; namespace Dalamud.Interface.ManagedFontAtlas.Internals; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 98a823deb..ce67b0eec 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -10,6 +10,7 @@ using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -20,7 +21,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; internal abstract class FontHandle : IFontHandle { private const int NonMainThreadFontAccessWarningCheckInterval = 10000; - private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); + private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = []; private static long nextNonMainThreadFontAccessWarningCheck; private readonly List pushedFonts = new(8); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index f6904db7c..81c7d4d89 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -4,14 +4,15 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; +using System.Threading; using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Utility; + using Lumina.Data.Files; using Vector4 = System.Numerics.Vector4; @@ -101,9 +102,9 @@ internal class GamePrebakedFontHandle : FontHandle /// internal sealed class HandleManager : IFontHandleManager { - private readonly Dictionary gameFontsRc = new(); - private readonly HashSet handles = new(); - private readonly object syncRoot = new(); + private readonly Dictionary gameFontsRc = []; + private readonly HashSet handles = []; + private readonly Lock syncRoot = new(); /// /// Initializes a new instance of the class. @@ -188,11 +189,11 @@ internal class GamePrebakedFontHandle : FontHandle private readonly HashSet gameFontStyles; // Owned by this class, but ImFontPtr values still do not belong to this. - private readonly Dictionary fonts = new(); - private readonly Dictionary buildExceptions = new(); - private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new(); + private readonly Dictionary fonts = []; + private readonly Dictionary buildExceptions = []; + private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = []; - private readonly HashSet templatedFonts = new(); + private readonly HashSet templatedFonts = []; /// /// Initializes a new instance of the class. @@ -415,7 +416,7 @@ internal class GamePrebakedFontHandle : FontHandle DalamudAsset.NotoSansJpMedium, new() { - GlyphRanges = new ushort[] { ' ', ' ', '\0' }, + GlyphRanges = [' ', ' ', '\0'], SizePx = sizePx, }); this.templatedFonts.Add(font); @@ -449,8 +450,8 @@ internal class GamePrebakedFontHandle : FontHandle public readonly GameFontStyle BaseStyle; public readonly GameFontFamilyAndSizeAttribute BaseAttr; public readonly int TexCount; - public readonly Dictionary Ranges = new(); - public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new(); + public readonly Dictionary Ranges = []; + public readonly List<(int RectId, int FdtGlyphIndex)> Rects = []; public readonly ushort[] RectLookup = new ushort[0x10000]; public readonly FdtFileView Fdt; public readonly ImFontPtr FullRangeFont; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs index b7f5ad12b..9eb90fe16 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs @@ -34,8 +34,7 @@ internal class LockedImFont : ILockedImFont /// public ILockedImFont NewRef() { - if (this.owner is null) - throw new ObjectDisposedException(nameof(LockedImFont)); + ObjectDisposedException.ThrowIf(this.owner == null, this); var newRef = new LockedImFont(this.ImFont, this.owner); this.owner.AddRef(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs index 0d6ad5c7c..ed40ce28b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Utility; using Microsoft.Extensions.ObjectPool; diff --git a/Dalamud/Interface/Style/DalamudColors.cs b/Dalamud/Interface/Style/DalamudColors.cs index aa3339c19..e4a8c5b9b 100644 --- a/Dalamud/Interface/Style/DalamudColors.cs +++ b/Dalamud/Interface/Style/DalamudColors.cs @@ -1,6 +1,7 @@ -using System.Numerics; +using System.Numerics; using Dalamud.Interface.Colors; + using Newtonsoft.Json; namespace Dalamud.Interface.Style; diff --git a/Dalamud/Interface/Style/StyleModel.cs b/Dalamud/Interface/Style/StyleModel.cs index bfce480f2..4c64e3a21 100644 --- a/Dalamud/Interface/Style/StyleModel.cs +++ b/Dalamud/Interface/Style/StyleModel.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -6,7 +6,9 @@ using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Utility; + using Newtonsoft.Json; + using Serilog; namespace Dalamud.Interface.Style; @@ -68,7 +70,7 @@ public abstract class StyleModel /// Thrown in case the version of the model is not known. public static StyleModel? Deserialize(string model) { - var json = Util.DecompressString(Convert.FromBase64String(model.Substring(3))); + var json = Util.DecompressString(Convert.FromBase64String(model[3..])); if (model.StartsWith(StyleModelV1.SerializedPrefix)) return JsonConvert.DeserializeObject(json); @@ -86,8 +88,7 @@ public abstract class StyleModel if (configuration.SavedStylesOld == null) return; - configuration.SavedStyles = new List(); - configuration.SavedStyles.AddRange(configuration.SavedStylesOld); + configuration.SavedStyles = [.. configuration.SavedStylesOld]; Log.Information("Transferred {NumStyles} styles", configuration.SavedStyles.Count); @@ -102,16 +103,11 @@ public abstract class StyleModel /// Thrown when the version of the style model is unknown. public string Serialize() { - string prefix; - switch (this) + var prefix = this switch { - case StyleModelV1: - prefix = StyleModelV1.SerializedPrefix; - break; - default: - throw new ArgumentOutOfRangeException(); - } - + StyleModelV1 => StyleModelV1.SerializedPrefix, + _ => throw new ArgumentOutOfRangeException(), + }; return prefix + Convert.ToBase64String(Util.CompressString(JsonConvert.SerializeObject(this))); } diff --git a/Dalamud/Interface/Style/StyleModelV1.cs b/Dalamud/Interface/Style/StyleModelV1.cs index 8c1de86f3..4af2512fd 100644 --- a/Dalamud/Interface/Style/StyleModelV1.cs +++ b/Dalamud/Interface/Style/StyleModelV1.cs @@ -3,6 +3,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; + using Newtonsoft.Json; namespace Dalamud.Interface.Style; @@ -17,7 +18,7 @@ public class StyleModelV1 : StyleModel /// private StyleModelV1() { - this.Colors = new Dictionary(); + this.Colors = []; this.Name = "Unknown"; } @@ -396,7 +397,7 @@ public class StyleModelV1 : StyleModel model.SelectableTextAlign = style.SelectableTextAlign; model.DisplaySafeAreaPadding = style.DisplaySafeAreaPadding; - model.Colors = new Dictionary(); + model.Colors = []; foreach (var imGuiCol in Enum.GetValues()) { diff --git a/Dalamud/Interface/Textures/ForwardingSharedImmediateTexture.cs b/Dalamud/Interface/Textures/ForwardingSharedImmediateTexture.cs index 12e312b3e..45dc69bbd 100644 --- a/Dalamud/Interface/Textures/ForwardingSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/ForwardingSharedImmediateTexture.cs @@ -1,8 +1,7 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Textures.TextureWraps; -using Dalamud.Storage.Assets; namespace Dalamud.Interface.Textures; diff --git a/Dalamud/Interface/Textures/ISharedImmediateTexture.cs b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs index b6aa4da83..7f3b54c97 100644 --- a/Dalamud/Interface/Textures/ISharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/ISharedImmediateTexture.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Utility; diff --git a/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs index d04688fe4..248fbbc67 100644 --- a/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs +++ b/Dalamud/Interface/Textures/ImGuiViewportTextureArgs.cs @@ -2,9 +2,7 @@ using System.Numerics; using System.Text; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; -using TerraFX.Interop.DirectX; namespace Dalamud.Interface.Textures; diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index c95a9b0ad..511f6e110 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -3,7 +3,6 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; @@ -36,10 +35,7 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe /// protected override async Task CreateTextureAsync(CancellationToken cancellationToken) { - await using var stream = this.assembly.GetManifestResourceStream(this.name); - if (stream is null) - throw new FileNotFoundException("The resource file could not be found."); - + await using var stream = this.assembly.GetManifestResourceStream(this.name) ?? throw new FileNotFoundException("The resource file could not be found."); var tm = await Service.GetAsync(); var ms = new MemoryStream(stream.CanSeek ? checked((int)stream.Length) : 0); await stream.CopyToAsync(ms, cancellationToken); diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index 5f9925ed3..931a1a73b 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -23,8 +23,8 @@ internal abstract class SharedImmediateTexture private static long instanceCounter; - private readonly object reviveLock = new(); - private readonly List ownerPlugins = new(); + private readonly Lock reviveLock = new(); + private readonly List ownerPlugins = []; private bool resourceReleased; private int refCount; @@ -476,8 +476,8 @@ internal abstract class SharedImmediateTexture { var ownerCopy = this.owner; var wrapCopy = this.innerWrap; - if (ownerCopy is null || wrapCopy is null) - throw new ObjectDisposedException(nameof(RefCountableWrappingTextureWrap)); + + ObjectDisposedException.ThrowIf(ownerCopy is null || wrapCopy is null, this); ownerCopy.AddRef(); return new RefCountableWrappingTextureWrap(wrapCopy, ownerCopy); diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs index fde40d462..ed1824e5c 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; @@ -44,7 +43,7 @@ internal sealed partial class TextureManager /// Gets the list containing all the loaded textures from plugins. /// Returned value must be used inside a lock. - public List BlameTracker { get; } = new(); + public List BlameTracker { get; } = []; /// Gets the blame for a texture wrap. /// The texture wrap. @@ -234,7 +233,7 @@ internal sealed partial class TextureManager public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); /// - public List OwnerPlugins { get; } = new(); + public List OwnerPlugins { get; } = []; /// public nint ResourceAddress => (nint)this.tex2D; diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs index b4573f04f..e803a1d13 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs @@ -5,6 +5,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Storage.Assets; using Dalamud.Utility; + using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs b/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs index 0796ad6cc..fe0f390eb 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.GamePath.cs @@ -113,8 +113,8 @@ internal sealed partial class TextureManager var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; type ??= string.Empty; - if (type.Length > 0 && !type.EndsWith("/")) - type += "/"; + if (type.Length > 0 && !type.EndsWith('/')) + type += '/'; return string.Format(format, iconId / 1000, type, iconId); } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs index d7e185b68..85cf3a1ca 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -5,7 +5,6 @@ using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; -using System.Threading.Tasks; using BitFaster.Caching.Lru; @@ -65,7 +64,7 @@ internal sealed partial class TextureManager private readonly ConcurrentDictionary gameDict = new(); private readonly ConcurrentDictionary fileDict = new(); private readonly ConcurrentDictionary<(Assembly, string), SharedImmediateTexture> manifestResourceDict = new(); - private readonly HashSet invalidatedTextures = new(); + private readonly HashSet invalidatedTextures = []; private readonly Thread sharedTextureReleaseThread; diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index d0f0d8c07..982b5c58d 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -34,7 +34,7 @@ internal sealed partial class TextureManager ITextureSubstitutionProvider, ITextureReadbackProvider { - private static readonly ModuleLog Log = new(nameof(TextureManager)); + private static readonly ModuleLog Log = ModuleLog.Create(); [ServiceManager.ServiceDependency] private readonly Dalamud dalamud = Service.Get(); @@ -382,10 +382,10 @@ internal sealed partial class TextureManager var tf = new TexFile(); typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( tf, - new object?[] { bytesArray }); + [bytesArray]); typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( tf, - new object?[] { new LuminaBinaryReader(bytesArray) }); + [new LuminaBinaryReader(bytesArray)]); // Note: FileInfo and FilePath are not used from TexFile; skip it. var wrap = this.NoThrottleCreateFromTexFile(tf); diff --git a/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs index 342397c5e..2cb1deac5 100644 --- a/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs @@ -3,7 +3,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps.Internal; using TerraFX.Interop.Windows; diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs index 3e0f31eca..d4407d76a 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs @@ -9,6 +9,7 @@ using Dalamud.Interface.Textures.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; using Dalamud.Utility; + using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs index 586d65559..cf849a1ef 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -22,7 +22,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu /// internal const uint TextureSize = 64; - private readonly List entries = new(); + private readonly List entries = []; private TitleScreenMenuEntry[]? entriesView; [ServiceManager.ServiceConstructor] @@ -42,7 +42,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu { lock (this.entries) { - if (!this.entries.Any()) + if (this.entries.Count == 0) return Array.Empty(); return this.entriesView ??= this.entries.OrderByDescending(x => x.IsInternal).ToArray(); @@ -59,7 +59,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu { lock (this.entries) { - if (!this.entries.Any()) + if (this.entries.Count == 0) return Array.Empty(); return this.entriesView ??= this.entries.OrderByDescending(x => x.IsInternal).ToArray(); @@ -81,7 +81,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu lock (this.entries) { var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == Assembly.GetCallingAssembly()).ToList(); - var priority = entriesOfAssembly.Any() + var priority = entriesOfAssembly.Count != 0 ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) : 0; entry = new(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); @@ -191,7 +191,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu lock (this.entries) { var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == null).ToList(); - var priority = entriesOfAssembly.Any() + var priority = entriesOfAssembly.Count != 0 ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) : 0; entry = new(null, priority, text, texture, onTriggered, showConditionKeys) @@ -220,7 +220,7 @@ internal class TitleScreenMenuPluginScoped : IInternalDisposableService, ITitleS [ServiceManager.ServiceDependency] private readonly TitleScreenMenu titleScreenMenuService = Service.Get(); - private readonly List pluginEntries = new(); + private readonly List pluginEntries = []; /// public IReadOnlyList? Entries => this.titleScreenMenuService.Entries; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index ea0e21e97..d2b4b655e 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -15,6 +15,7 @@ using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface; @@ -601,7 +602,7 @@ public sealed class UiBuilder : IDisposable, IUiBuilder /// /// Gets or sets a history of the last draw times, used to calculate an average. /// - internal List DrawTimeHistory { get; set; } = new List(); + internal List DrawTimeHistory { get; set; } = []; private InterfaceManager? InterfaceManagerWithScene => Service.GetNullable()?.Manager; diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index 85a2d8344..48c6a114d 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Utility; + using Lumina.Data.Files; using Lumina.Data.Parsing.Uld; @@ -17,7 +18,7 @@ public class UldWrapper : IDisposable { private readonly DataManager data; private readonly TextureManager textureManager; - private readonly Dictionary textures = new(); + private readonly Dictionary textures = []; /// Initializes a new instance of the class, wrapping an ULD file. /// The UiBuilder used to load textures. diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs index 4a0137c88..7fcf795aa 100644 --- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs +++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs @@ -14,7 +14,9 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; + using Serilog; + using TerraFX.Interop.Windows; namespace Dalamud.Interface.Utility.Internal; diff --git a/Dalamud/Interface/Utility/Raii/Color.cs b/Dalamud/Interface/Utility/Raii/Color.cs index 7bf2efc38..9682b929e 100644 --- a/Dalamud/Interface/Utility/Raii/Color.cs +++ b/Dalamud/Interface/Utility/Raii/Color.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; -using Dalamud.Utility; namespace Dalamud.Interface.Utility.Raii; @@ -29,7 +28,7 @@ public static partial class ImRaii public sealed class Color : IDisposable { - internal static readonly List<(ImGuiCol, uint)> Stack = new(); + internal static readonly List<(ImGuiCol, uint)> Stack = []; private int count; public Color Push(ImGuiCol idx, uint color, bool condition = true) diff --git a/Dalamud/Interface/Utility/Raii/EndObjects.cs b/Dalamud/Interface/Utility/Raii/EndObjects.cs index 80122360c..80ae495e2 100644 --- a/Dalamud/Interface/Utility/Raii/EndObjects.cs +++ b/Dalamud/Interface/Utility/Raii/EndObjects.cs @@ -193,8 +193,8 @@ public static partial class ImRaii return new EndUnconditionally(Restore, true); } - private static IEndObject DisabledEnd() - => new EndUnconditionally(() => + private static EndUnconditionally DisabledEnd() + => new(() => { --disabledCount; ImGui.EndDisabled(); diff --git a/Dalamud/Interface/Utility/Raii/Font.cs b/Dalamud/Interface/Utility/Raii/Font.cs index da35c693a..1b5e8cc58 100644 --- a/Dalamud/Interface/Utility/Raii/Font.cs +++ b/Dalamud/Interface/Utility/Raii/Font.cs @@ -1,5 +1,4 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Utility; namespace Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Utility/Raii/Plot.cs b/Dalamud/Interface/Utility/Raii/Plot.cs index d2ff38299..e88919e48 100644 --- a/Dalamud/Interface/Utility/Raii/Plot.cs +++ b/Dalamud/Interface/Utility/Raii/Plot.cs @@ -96,7 +96,7 @@ public static partial class ImRaii public sealed class PlotStyle : IDisposable { - internal static readonly List<(ImPlotStyleVar, Vector2)> Stack = new(); + internal static readonly List<(ImPlotStyleVar, Vector2)> Stack = []; private int count; @@ -249,7 +249,7 @@ public static partial class ImRaii public sealed class PlotColor : IDisposable { - internal static readonly List<(ImPlotCol, uint)> Stack = new(); + internal static readonly List<(ImPlotCol, uint)> Stack = []; private int count; // Reimplementation of https://github.com/ocornut/imgui/blob/868facff9ded2d61425c67deeba354eb24275bd1/imgui.cpp#L3035 diff --git a/Dalamud/Interface/Utility/Raii/Style.cs b/Dalamud/Interface/Utility/Raii/Style.cs index bfd04ea3c..e178b68d3 100644 --- a/Dalamud/Interface/Utility/Raii/Style.cs +++ b/Dalamud/Interface/Utility/Raii/Style.cs @@ -35,7 +35,7 @@ public static partial class ImRaii public sealed class Style : IDisposable { - internal static readonly List<(ImGuiStyleVar, Vector2)> Stack = new(); + internal static readonly List<(ImGuiStyleVar, Vector2)> Stack = []; private int count; diff --git a/Dalamud/Interface/Windowing/Persistence/PresetModel.cs b/Dalamud/Interface/Windowing/Persistence/PresetModel.cs index 4ddf55e51..1c6a93c16 100644 --- a/Dalamud/Interface/Windowing/Persistence/PresetModel.cs +++ b/Dalamud/Interface/Windowing/Persistence/PresetModel.cs @@ -25,7 +25,7 @@ internal class PresetModel /// Gets or sets a dictionary containing the windows in the preset, mapping their ID to the preset. /// [JsonProperty("w")] - public Dictionary Windows { get; set; } = new(); + public Dictionary Windows { get; set; } = []; /// /// Class representing a window in a preset. diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 5a79a017a..3cbd1a521 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Numerics; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Colors; @@ -33,7 +35,7 @@ public abstract class Window private const float FadeInOutTime = 0.072f; private const string AdditionsPopupName = "WindowSystemContextActions"; - private static readonly ModuleLog Log = new("WindowSystem"); + private static readonly ModuleLog Log = ModuleLog.Create(); private static bool wasEscPressedLastFrame = false; @@ -261,7 +263,7 @@ public abstract class Window /// disabled globally by the user, an internal title bar button to manage these is added when drawing, but it will /// not appear in this collection. If you wish to remove this button, set both of these values to false. /// - public List TitleBarButtons { get; set; } = new(); + public List TitleBarButtons { get; set; } = []; /// /// Gets or sets a value indicating whether this window will stay open. diff --git a/Dalamud/Interface/Windowing/WindowSystem.cs b/Dalamud/Interface/Windowing/WindowSystem.cs index d6e9649bb..319640336 100644 --- a/Dalamud/Interface/Windowing/WindowSystem.cs +++ b/Dalamud/Interface/Windowing/WindowSystem.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Interface.Windowing.Persistence; + using Serilog; namespace Dalamud.Interface.Windowing; @@ -15,7 +16,7 @@ public class WindowSystem { private static DateTimeOffset lastAnyFocus; - private readonly List windows = new(); + private readonly List windows = []; private string lastFocusedWindowName = string.Empty; diff --git a/Dalamud/IoC/Internal/ObjectInstance.cs b/Dalamud/IoC/Internal/ObjectInstance.cs index 3a963f6bd..af97b7124 100644 --- a/Dalamud/IoC/Internal/ObjectInstance.cs +++ b/Dalamud/IoC/Internal/ObjectInstance.cs @@ -1,4 +1,3 @@ -using System.Reflection; using System.Threading.Tasks; namespace Dalamud.IoC.Internal; diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 6383b6b11..0dacacc54 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -20,10 +20,10 @@ namespace Dalamud.IoC.Internal; [ServiceManager.ProvidedService] internal class ServiceContainer : IServiceType { - private static readonly ModuleLog Log = new("SERVICECONTAINER"); + private static readonly ModuleLog Log = ModuleLog.Create(); - private readonly Dictionary instances = new(); - private readonly Dictionary interfaceToTypeMap = new(); + private readonly Dictionary instances = []; + private readonly Dictionary interfaceToTypeMap = []; /// /// Initializes a new instance of the class. diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index 0a7086e73..8c7368c3f 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -18,7 +18,7 @@ public class Localization : IServiceType /// /// Array of language codes which have a valid translation in Dalamud. /// - public static readonly string[] ApplicableLangCodes = { "de", "ja", "fr", "it", "es", "ko", "no", "ru", "zh", "tw" }; + public static readonly string[] ApplicableLangCodes = ["de", "ja", "fr", "it", "es", "ko", "no", "ru", "zh", "tw"]; private const string FallbackLangCode = "en"; diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index cb9a0db6d..c6bd895a0 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -15,8 +15,8 @@ namespace Dalamud.Logging.Internal; [ServiceManager.EarlyLoadedService] internal class TaskTracker : IInternalDisposableService { - private static readonly ModuleLog Log = new("TT"); - private static readonly List TrackedTasksInternal = new(); + private static readonly ModuleLog Log = ModuleLog.Create(); + private static readonly List TrackedTasksInternal = []; private static readonly ConcurrentQueue NewlyCreatedTasks = new(); private static bool clearRequested = false; diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs index 2eae1be6d..d23b67e4f 100644 --- a/Dalamud/Memory/MemoryHelper.cs +++ b/Dalamud/Memory/MemoryHelper.cs @@ -6,11 +6,15 @@ using System.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Memory.Exceptions; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.String; + using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; + using Microsoft.Extensions.ObjectPool; + using Windows.Win32.Foundation; using Windows.Win32.System.Memory; diff --git a/Dalamud/Networking/Http/HappyEyeballsCallback.cs b/Dalamud/Networking/Http/HappyEyeballsCallback.cs index 4e3ee61f6..59bdff630 100644 --- a/Dalamud/Networking/Http/HappyEyeballsCallback.cs +++ b/Dalamud/Networking/Http/HappyEyeballsCallback.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -20,7 +20,7 @@ namespace Dalamud.Networking.Http; /// public class HappyEyeballsCallback : IDisposable { - private static readonly ModuleLog Log = new("HTTP"); + private static readonly ModuleLog Log = ModuleLog.Create(); /* * ToDo: Eventually add in some kind of state management to cache DNS and IP Family. diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index e42bbe608..df1d0f6e9 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -342,7 +342,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa { var mi = this.configs.GetType().GetMethod("LoadForType"); var fn = mi.MakeGenericMethod(type); - return (IPluginConfiguration)fn.Invoke(this.configs, new object[] { this.plugin.InternalName }); + return (IPluginConfiguration)fn.Invoke(this.configs, [this.plugin.InternalName]); } } diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 3fc011a68..734bc5ef9 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Console; @@ -32,7 +33,7 @@ namespace Dalamud.Plugin.Internal.AutoUpdate; [ServiceManager.EarlyLoadedService] internal class AutoUpdateManager : IServiceType { - private static readonly ModuleLog Log = new("AUTOUPDATE"); + private static readonly ModuleLog Log = ModuleLog.Create(); /// /// Time we should wait after login to update. @@ -376,7 +377,7 @@ internal class AutoUpdateManager : IServiceType } } - private void NotifyUpdatesAreAvailable(ICollection updatablePlugins) + private void NotifyUpdatesAreAvailable(List updatablePlugins) { if (updatablePlugins.Count == 0) return; diff --git a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs index 1c6e6feed..aa304cd05 100644 --- a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs +++ b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs @@ -15,9 +15,9 @@ namespace Dalamud.Plugin.Internal.Loader; /// internal class AssemblyLoadContextBuilder { - private readonly List additionalProbingPaths = new(); - private readonly List resourceProbingPaths = new(); - private readonly List resourceProbingSubpaths = new(); + private readonly List additionalProbingPaths = []; + private readonly List resourceProbingPaths = []; + private readonly List resourceProbingSubpaths = []; private readonly Dictionary managedLibraries = new(StringComparer.Ordinal); private readonly Dictionary nativeLibraries = new(StringComparer.Ordinal); private readonly HashSet privateAssemblies = new(StringComparer.Ordinal); @@ -140,7 +140,7 @@ internal class AssemblyLoadContextBuilder return this; } - var names = new Queue(new[] { assemblyName }); + var names = new Queue([assemblyName]); while (names.TryDequeue(out var name)) { diff --git a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs index b863b8ee1..e26338820 100644 --- a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs +++ b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs @@ -39,13 +39,13 @@ internal class LoaderConfig /// /// Gets a list of assemblies which should be treated as private. /// - public ICollection PrivateAssemblies { get; } = new List(); + public ICollection PrivateAssemblies { get; } = []; /// /// Gets a list of assemblies which should be unified between the host and the plugin. /// /// what-are-shared-types - public ICollection<(AssemblyName Name, bool Recursive)> SharedAssemblies { get; } = new List<(AssemblyName Name, bool Recursive)>(); + public ICollection<(AssemblyName Name, bool Recursive)> SharedAssemblies { get; } = []; /// /// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host. diff --git a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs index 4ea4eb5c4..a85d20e40 100644 --- a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs +++ b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs @@ -24,7 +24,7 @@ internal class ManagedLoadContext : AssemblyLoadContext private readonly IReadOnlyDictionary managedAssemblies; private readonly IReadOnlyDictionary nativeLibraries; private readonly IReadOnlyCollection privateAssemblies; - private readonly ICollection defaultAssemblies; + private readonly List defaultAssemblies; private readonly IReadOnlyCollection additionalProbingPaths; private readonly bool preferDefaultLoadContext; private readonly string[] resourceRoots; @@ -64,8 +64,7 @@ internal class ManagedLoadContext : AssemblyLoadContext bool shadowCopyNativeLibraries) : base(Path.GetFileNameWithoutExtension(mainAssemblyPath), isCollectible) { - if (resourceProbingPaths == null) - throw new ArgumentNullException(nameof(resourceProbingPaths)); + ArgumentNullException.ThrowIfNull(resourceProbingPaths); this.mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath)); this.dependencyResolver = new AssemblyDependencyResolver(mainAssemblyPath); @@ -243,7 +242,7 @@ internal class ManagedLoadContext : AssemblyLoadContext } // check to see if there is a library entry for the library without the file extension - var trimmedName = unmanagedDllName.Substring(0, unmanagedDllName.Length - suffix.Length); + var trimmedName = unmanagedDllName[..^suffix.Length]; if (this.nativeLibraries.TryGetValue(prefix + trimmedName, out library)) { diff --git a/Dalamud/Plugin/Internal/Loader/PlatformInformation.cs b/Dalamud/Plugin/Internal/Loader/PlatformInformation.cs index ec1d557be..151d184db 100644 --- a/Dalamud/Plugin/Internal/Loader/PlatformInformation.cs +++ b/Dalamud/Plugin/Internal/Loader/PlatformInformation.cs @@ -11,21 +11,21 @@ internal class PlatformInformation /// /// Gets a list of native OS specific library extensions. /// - public static string[] NativeLibraryExtensions => new[] { ".dll" }; + public static string[] NativeLibraryExtensions => [".dll"]; /// /// Gets a list of native OS specific library prefixes. /// - public static string[] NativeLibraryPrefixes => new[] { string.Empty }; + public static string[] NativeLibraryPrefixes => [string.Empty]; /// /// Gets a list of native OS specific managed assembly extensions. /// - public static string[] ManagedAssemblyExtensions => new[] - { + public static string[] ManagedAssemblyExtensions => + [ ".dll", ".ni.dll", ".exe", ".ni.exe", - }; + ]; } diff --git a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs index 54b9cad4b..a77bfe088 100644 --- a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs +++ b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs @@ -53,8 +53,7 @@ internal class PluginLoader : IDisposable /// A loader. public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Action configure) { - if (configure == null) - throw new ArgumentNullException(nameof(configure)); + ArgumentNullException.ThrowIfNull(configure); var config = new LoaderConfig(assemblyFile); configure(config); @@ -159,7 +158,6 @@ internal class PluginLoader : IDisposable private void EnsureNotDisposed() { - if (this.disposed) - throw new ObjectDisposedException(nameof(PluginLoader)); + ObjectDisposedException.ThrowIf(this.disposed, this); } } diff --git a/Dalamud/Plugin/Internal/PluginErrorHandler.cs b/Dalamud/Plugin/Internal/PluginErrorHandler.cs index 0094c3751..6733cc3ca 100644 --- a/Dalamud/Plugin/Internal/PluginErrorHandler.cs +++ b/Dalamud/Plugin/Internal/PluginErrorHandler.cs @@ -22,7 +22,7 @@ internal class PluginErrorHandler : IServiceType private readonly NotificationManager notificationManager; private readonly DalamudInterface di; - private readonly Dictionary invokerCache = new(); + private readonly Dictionary invokerCache = []; private DateTime lastErrorTime = DateTime.MinValue; private IActiveNotification? activeNotification; @@ -141,10 +141,7 @@ internal class PluginErrorHandler : IServiceType private static Action CreateInvoker() where TDelegate : Delegate { var delegateType = typeof(TDelegate); - var method = delegateType.GetMethod("Invoke"); - if (method == null) - throw new InvalidOperationException($"Delegate {delegateType} does not have an Invoke method."); - + var method = delegateType.GetMethod("Invoke") ?? throw new InvalidOperationException($"Delegate {delegateType} does not have an Invoke method."); var parameters = method.GetParameters(); // Create parameters for the lambda @@ -153,7 +150,7 @@ internal class PluginErrorHandler : IServiceType // Create expressions to convert array elements to parameter types var callArgs = new Expression[parameters.Length]; - for (int i = 0; i < parameters.Length; i++) + for (var i = 0; i < parameters.Length; i++) { var paramType = parameters[i].ParameterType; var arrayAccess = Expression.ArrayIndex(argsParam, Expression.Constant(i)); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 193a2d45f..0c9894380 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Configuration; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -31,6 +32,7 @@ using Dalamud.Plugin.Ipc.Internal; using Dalamud.Support; using Dalamud.Utility; using Dalamud.Utility.Timing; + using Newtonsoft.Json; namespace Dalamud.Plugin.Internal; @@ -56,9 +58,9 @@ internal class PluginManager : IInternalDisposableService private readonly DirectoryInfo pluginDirectory; private readonly BannedPlugin[]? bannedPlugins; - private readonly List installedPluginsList = new(); - private readonly List availablePluginsList = new(); - private readonly List updatablePluginsList = new(); + private readonly List installedPluginsList = []; + private readonly List availablePluginsList = []; + private readonly List updatablePluginsList = []; private readonly Task openInstallerWindowPluginChangelogsLink; @@ -131,7 +133,7 @@ internal class PluginManager : IInternalDisposableService PluginInstallerOpenKind.Changelogs); })); - this.configuration.PluginTestingOptIns ??= new(); + this.configuration.PluginTestingOptIns ??= []; this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient); registerStartupBlocker( @@ -230,7 +232,7 @@ internal class PluginManager : IInternalDisposableService /// /// Gets a list of all plugin repositories. The main repo should always be first. /// - public List Repos { get; private set; } = new(); + public List Repos { get; private set; } = []; /// /// Gets a value indicating whether plugins are not still loading from boot. @@ -1514,11 +1516,7 @@ internal class PluginManager : IInternalDisposableService JsonConvert.SerializeObject(repoManifest, Formatting.Indented)); // Reload as a local manifest, add some attributes, and save again. - var tempManifest = LocalPluginManifest.Load(tempManifestFile); - - if (tempManifest == null) - throw new Exception("Plugin had no valid manifest"); - + var tempManifest = LocalPluginManifest.Load(tempManifestFile) ?? throw new Exception("Plugin had no valid manifest"); if (tempManifest.InternalName != repoManifest.InternalName) { throw new Exception( @@ -1897,9 +1895,9 @@ internal class PluginManager : IInternalDisposableService /// public class StartupLoadTracker { - private readonly Dictionary internalToPublic = new(); - private readonly ConcurrentBag allInternalNames = new(); - private readonly ConcurrentBag finishedInternalNames = new(); + private readonly Dictionary internalToPublic = []; + private readonly ConcurrentBag allInternalNames = []; + private readonly ConcurrentBag finishedInternalNames = []; /// /// Gets a value indicating the total load progress. diff --git a/Dalamud/Plugin/Internal/PluginValidator.cs b/Dalamud/Plugin/Internal/PluginValidator.cs index b2cbe5520..4c6bb9fef 100644 --- a/Dalamud/Plugin/Internal/PluginValidator.cs +++ b/Dalamud/Plugin/Internal/PluginValidator.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Dalamud.Game.Command; @@ -11,7 +11,7 @@ namespace Dalamud.Plugin.Internal; /// internal static class PluginValidator { - private static readonly char[] LineSeparator = new[] { ' ', '\n', '\r' }; + private static readonly char[] LineSeparator = [' ', '\n', '\r']; /// /// Represents the severity of a validation problem. diff --git a/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs index 09cceebcb..6608f2669 100644 --- a/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs @@ -3,12 +3,14 @@ using System.Linq; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Game.Gui; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; + using Serilog; namespace Dalamud.Plugin.Internal.Profiles; @@ -39,7 +41,7 @@ internal class PluginManagementCommandHandler : IInternalDisposableService private readonly ChatGui chat; private readonly Framework framework; - private List<(Target Target, PluginCommandOperation Operation)> commandQueue = new(); + private List<(Target Target, PluginCommandOperation Operation)> commandQueue = []; /// /// Initializes a new instance of the class. diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index d899b0cca..9d931d9ed 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -14,7 +14,7 @@ namespace Dalamud.Plugin.Internal.Profiles; /// internal class Profile { - private static readonly ModuleLog Log = new("PROFILE"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly ProfileManager manager; private readonly ProfileModelV1 modelV1; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 775ff7a72..bbe678162 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -1,10 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using CheapLoc; + using Dalamud.Configuration.Internal; using Dalamud.Logging.Internal; using Dalamud.Utility; @@ -15,12 +16,12 @@ namespace Dalamud.Plugin.Internal.Profiles; /// Class responsible for managing plugin profiles. /// [ServiceManager.BlockingEarlyLoadedService($"Data provider for {nameof(PluginManager)}.")] -internal class ProfileManager : IServiceType +internal partial class ProfileManager : IServiceType { - private static readonly ModuleLog Log = new("PROFMAN"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly DalamudConfiguration config; - private readonly List profiles = new(); + private readonly List profiles = []; private volatile bool isBusy = false; @@ -151,11 +152,7 @@ internal class ProfileManager : IServiceType /// The newly cloned profile. public Profile CloneProfile(Profile toClone) { - var newProfile = this.ImportProfile(toClone.Model.SerializeForShare()); - if (newProfile == null) - throw new Exception("New profile was null while cloning"); - - return newProfile; + return this.ImportProfile(toClone.Model.SerializeForShare()) ?? throw new Exception("New profile was null while cloning"); } /// @@ -338,12 +335,15 @@ internal class ProfileManager : IServiceType } } + [GeneratedRegex(@" \(.* Mix\)")] + private static partial Regex MixRegex(); + private string GenerateUniqueProfileName(string startingWith) { if (this.profiles.All(x => x.Name != startingWith)) return startingWith; - startingWith = Regex.Replace(startingWith, @" \(.* Mix\)", string.Empty); + startingWith = MixRegex().Replace(startingWith, string.Empty); while (true) { @@ -359,7 +359,7 @@ internal class ProfileManager : IServiceType this.config.DefaultProfile ??= new ProfileModelV1(); this.profiles.Add(new Profile(this, this.config.DefaultProfile, true, true)); - this.config.SavedProfiles ??= new List(); + this.config.SavedProfiles ??= []; foreach (var profileModel in this.config.SavedProfiles) { this.profiles.Add(new Profile(this, profileModel, false, true)); diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs index e3d9e2955..37487e321 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Reflection; using Dalamud.Utility; + using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -32,7 +33,7 @@ public abstract class ProfileModel /// Thrown when the parsed string is not a valid profile. public static ProfileModel? Deserialize(string model) { - var json = Util.DecompressString(Convert.FromBase64String(model.Substring(3))); + var json = Util.DecompressString(Convert.FromBase64String(model[3..])); if (model.StartsWith(ProfileModelV1.SerializedPrefix)) return JsonConvert.DeserializeObject(json); @@ -47,19 +48,15 @@ public abstract class ProfileModel /// Thrown when an unsupported model is serialized. public string SerializeForShare() { - string prefix; - switch (this) + var prefix = this switch { - case ProfileModelV1: - prefix = ProfileModelV1.SerializedPrefix; - break; - default: - throw new ArgumentOutOfRangeException(); - } + ProfileModelV1 => ProfileModelV1.SerializedPrefix, + _ => throw new ArgumentOutOfRangeException(), + }; // HACK: Just filter the ID for now, we should split the sharing + saving model var serialized = JsonConvert.SerializeObject(this, new JsonSerializerSettings() - { ContractResolver = new IgnorePropertiesResolver(new[] { "WorkingPluginId" }) }); + { ContractResolver = new IgnorePropertiesResolver(["WorkingPluginId"]) }); return prefix + Convert.ToBase64String(Util.CompressString(serialized)); } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index a1a327c1d..deee9c04f 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json; @@ -63,7 +63,7 @@ public class ProfileModelV1 : ProfileModel /// /// Gets or sets the list of plugins in this profile. /// - public List Plugins { get; set; } = new(); + public List Plugins { get; set; } = []; /// /// Class representing a single plugin in a profile. diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 34b54163a..2e20d2aef 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -17,7 +17,7 @@ namespace Dalamud.Plugin.Internal.Types; /// internal sealed class LocalDevPlugin : LocalPlugin { - private static readonly ModuleLog Log = new("PLUGIN"); + private static readonly ModuleLog Log = ModuleLog.Create(); // Ref to Dalamud.Configuration.DevPluginSettings private readonly DevPluginSettings devSettings; diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 1fe18b95b..29867b355 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -33,7 +33,7 @@ internal class LocalPlugin : IAsyncDisposable protected LocalPluginManifest manifest; #pragma warning restore SA1401 - private static readonly ModuleLog Log = new("LOCALPLUGIN"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly FileInfo manifestFile; private readonly FileInfo disabledFile; diff --git a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs index 3aededa18..73d23d19f 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs @@ -1,7 +1,9 @@ using System.IO; using Dalamud.Utility; + using Newtonsoft.Json; + using Serilog; namespace Dalamud.Plugin.Internal.Types.Manifest; diff --git a/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs index 47e92cd84..b83ba91a8 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; + using Newtonsoft.Json; namespace Dalamud.Plugin.Internal.Types.Manifest; diff --git a/Dalamud/Plugin/Internal/Types/PluginDef.cs b/Dalamud/Plugin/Internal/Types/PluginDef.cs index 25cd82423..f9f49225a 100644 --- a/Dalamud/Plugin/Internal/Types/PluginDef.cs +++ b/Dalamud/Plugin/Internal/Types/PluginDef.cs @@ -7,7 +7,7 @@ namespace Dalamud.Plugin.Internal.Types; /// /// Plugin Definition. /// -internal struct PluginDef +internal readonly struct PluginDef { /// /// Initializes a new instance of the struct. diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index cbf69bb5e..fc9f4e372 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Dalamud.Common.Game; using Dalamud.Plugin.Internal.Types.Manifest; + using Newtonsoft.Json; namespace Dalamud.Plugin.Internal.Types; diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index d5c1131af..d47a3727d 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -29,7 +29,7 @@ internal class PluginRepository private const int HttpRequestTimeoutSeconds = 20; - private static readonly ModuleLog Log = new("PLUGINR"); + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly HttpClient httpClient; /// @@ -119,13 +119,7 @@ internal class PluginRepository response.EnsureSuccessStatusCode(); var data = await response.Content.ReadAsStringAsync(); - var pluginMaster = JsonConvert.DeserializeObject>(data); - - if (pluginMaster == null) - { - throw new Exception("Deserialized PluginMaster was null."); - } - + var pluginMaster = JsonConvert.DeserializeObject>(data) ?? throw new Exception("Deserialized PluginMaster was null."); pluginMaster.Sort((pm1, pm2) => string.Compare(pm1.Name, pm2.Name, StringComparison.Ordinal)); // Set the source for each remote manifest. Allows for checking if is 3rd party. diff --git a/Dalamud/Plugin/Ipc/ICallGateProvider.cs b/Dalamud/Plugin/Ipc/ICallGateProvider.cs index 387f0adf9..5fde2ef01 100644 --- a/Dalamud/Plugin/Ipc/ICallGateProvider.cs +++ b/Dalamud/Plugin/Ipc/ICallGateProvider.cs @@ -1,5 +1,4 @@ using Dalamud.Plugin.Ipc.Internal; -using Dalamud.Utility; #pragma warning disable SA1402 // File may only contain a single type diff --git a/Dalamud/Plugin/Ipc/Internal/CallGate.cs b/Dalamud/Plugin/Ipc/Internal/CallGate.cs index fef4b97d0..3f299da9d 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGate.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGate.cs @@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Ipc.Internal; [ServiceManager.EarlyLoadedService] internal class CallGate : IServiceType { - private readonly Dictionary gates = new(); + private readonly Dictionary gates = []; private ImmutableDictionary? gatesCopy; diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index 8bd631b0e..78c7f841b 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -9,6 +9,7 @@ using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal.Converters; using Newtonsoft.Json; + using Serilog; namespace Dalamud.Plugin.Ipc.Internal; @@ -23,7 +24,7 @@ internal class CallGateChannel /// /// The actual storage. /// - private readonly HashSet subscriptions = new(); + private readonly HashSet subscriptions = []; /// /// A copy of the actual storage, that will be cleared and populated depending on changes made to diff --git a/Dalamud/Plugin/Ipc/Internal/DataCache.cs b/Dalamud/Plugin/Ipc/Internal/DataCache.cs index 38cea4866..d565c8b35 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataCache.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -40,7 +40,7 @@ internal readonly struct DataCache { this.Tag = tag; this.CreatorAssemblyName = creatorAssemblyName; - this.UserAssemblyNames = new(); + this.UserAssemblyNames = []; this.Data = data; this.Type = type; } diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index 8ce5ddb95..becbe1211 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -19,7 +19,7 @@ internal class DataShare : IServiceType /// Dictionary of cached values. Note that is being used, as it does its own locking, /// effectively preventing calling the data generator multiple times concurrently. /// - private readonly Dictionary> caches = new(); + private readonly Dictionary> caches = []; [ServiceManager.ServiceConstructor] private DataShare() diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs index ca0c8ff92..16d16056d 100644 --- a/Dalamud/SafeMemory.cs +++ b/Dalamud/SafeMemory.cs @@ -193,7 +193,7 @@ public static class SafeMemory return null; var data = encoding.GetString(buffer); var eosPos = data.IndexOf('\0'); - return eosPos == -1 ? data : data.Substring(0, eosPos); + return eosPos == -1 ? data : data[..eosPos]; } /// @@ -303,7 +303,7 @@ public static class SafeMemory return bytes; } - public T Read(int offset = 0) => (T)Marshal.PtrToStructure(this.hGlobal + offset, typeof(T)); + public T Read(int offset = 0) => Marshal.PtrToStructure(this.hGlobal + offset); public object? Read(Type type, int offset = 0) => Marshal.PtrToStructure(this.hGlobal + offset, type); diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 88c6366fd..824f816ff 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -35,7 +35,7 @@ internal static class ServiceManager /// /// Static log facility for Service{T}, to avoid duplicate instances for different types. /// - public static readonly ModuleLog Log = new("SVC"); + public static readonly ModuleLog Log = new(nameof(ServiceManager)); #if DEBUG /// @@ -44,7 +44,7 @@ internal static class ServiceManager internal static readonly ThreadLocal CurrentConstructorServiceType = new(); [SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Debugging purposes")] - private static readonly List LoadedServices = new(); + private static readonly List LoadedServices = []; #endif private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = @@ -291,7 +291,7 @@ internal static class ServiceManager await loadingDialog.HideAndJoin(); return; - async Task WaitWithTimeoutConsent(IEnumerable tasksEnumerable, LoadingDialog.State state) + static async Task WaitWithTimeoutConsent(IEnumerable tasksEnumerable, LoadingDialog.State state) { loadingDialog.CurrentState = state; var tasks = tasksEnumerable.AsReadOnlyCollection(); @@ -317,7 +317,7 @@ internal static class ServiceManager servicesToLoad.UnionWith(earlyLoadingServices); servicesToLoad.UnionWith(blockingEarlyLoadingServices); - while (servicesToLoad.Any()) + while (servicesToLoad.Count != 0) { foreach (var serviceType in servicesToLoad) { @@ -367,7 +367,7 @@ internal static class ServiceManager BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, - new object[] { startLoaderArgs })); + [startLoaderArgs])); servicesToLoad.Remove(serviceType); #if DEBUG @@ -383,23 +383,23 @@ internal static class ServiceManager #endif } - if (!tasks.Any()) + if (tasks.Count == 0) { // No more services we can start loading for now. // Either we're waiting for provided services, or there's a dependency cycle. providedServices.RemoveWhere(x => getAsyncTaskMap[x].IsCompleted); - if (providedServices.Any()) + if (providedServices.Count != 0) await Task.WhenAny(providedServices.Select(x => getAsyncTaskMap[x])); else throw new InvalidOperationException("Unresolvable dependency cycle detected"); continue; } - if (servicesToLoad.Any()) + if (servicesToLoad.Count != 0) { await Task.WhenAny(tasks); var faultedTasks = tasks.Where(x => x.IsFaulted).Select(x => (Exception)x.Exception!).ToArray(); - if (faultedTasks.Any()) + if (faultedTasks.Length != 0) throw new AggregateException(faultedTasks); } else @@ -426,7 +426,7 @@ internal static class ServiceManager await loadingDialog.HideAndJoin(); - while (tasks.Any()) + while (tasks.Count != 0) { await Task.WhenAny(tasks); tasks.RemoveAll(x => x.IsCompleted); diff --git a/Dalamud/Service/Service{T}.cs b/Dalamud/Service/Service{T}.cs index 1f5558893..c965eeb3d 100644 --- a/Dalamud/Service/Service{T}.cs +++ b/Dalamud/Service/Service{T}.cs @@ -206,7 +206,7 @@ internal static class Service where T : IServiceType is not ServiceManager.ServiceKind.BlockingEarlyLoadedService and not ServiceManager.ServiceKind.ProvidedService) .ToArray(); - if (offenders.Any()) + if (offenders.Length != 0) { const string bels = nameof(ServiceManager.BlockingEarlyLoadedServiceAttribute); const string ps = nameof(ServiceManager.ProvidedServiceAttribute); @@ -286,13 +286,13 @@ internal static class Service where T : IServiceType { if (method.Invoke(instance, args) is Task task) { - tasks ??= new(); + tasks ??= []; tasks.Add(task); } } catch (Exception e) { - tasks ??= new(); + tasks ??= []; tasks.Add(Task.FromException(e)); } } @@ -351,15 +351,12 @@ internal static class Service where T : IServiceType BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding; return typeof(T) .GetConstructors(ctorBindingFlags) - .SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any()); + .SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Length != 0); } private static async Task ConstructObject(IReadOnlyCollection additionalProvidedTypedObjects) { - var ctor = GetServiceConstructor(); - if (ctor == null) - throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor"); - + var ctor = GetServiceConstructor() ?? throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor"); var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects) .ConfigureAwait(false); using (Timings.Start($"{typeof(T).Name} Construct")) @@ -460,7 +457,7 @@ internal static class ServiceHelpers BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, - new object?[] { includeUnloadDependencies }) ?? new List(); + [includeUnloadDependencies]) ?? new List(); } /// diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs index b4dc41bfd..8b74e4347 100644 --- a/Dalamud/Storage/Assets/IDalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs @@ -1,9 +1,8 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.IO; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; namespace Dalamud.Storage.Assets; diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index d9f8526c3..72cc598ae 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -1,9 +1,11 @@ -using System.IO; +using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Utility; + using SQLite; namespace Dalamud.Storage; @@ -25,9 +27,9 @@ namespace Dalamud.Storage; [ServiceManager.ProvidedService] internal class ReliableFileStorage : IInternalDisposableService { - private static readonly ModuleLog Log = new("VFS"); + private static readonly ModuleLog Log = ModuleLog.Create(); - private readonly object syncRoot = new(); + private readonly Lock syncRoot = new(); private SQLiteConnection? db; @@ -272,8 +274,9 @@ internal class ReliableFileStorage : IInternalDisposableService throw new FileNotFoundException("Backup database was not available"); var normalizedPath = NormalizePath(path); - var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); - return file == null ? throw new FileNotFoundException() : file.Data; + var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId) + ?? throw new FileNotFoundException(); + return file.Data; } // If the file doesn't exist, immediately check the backup db diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index f0a98ca98..c37c4d2f8 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Dalamud.Networking.Http; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Utility; + using Newtonsoft.Json; namespace Dalamud.Support; diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index de529a29b..2dd0fb623 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -8,7 +8,9 @@ using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Utility; + using Newtonsoft.Json; + using Serilog; namespace Dalamud.Support; @@ -32,7 +34,7 @@ public static class Troubleshooting { LastException = exception; - var fixedContext = context?.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + var fixedContext = context?.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); try { @@ -127,7 +129,7 @@ public static class Troubleshooting public bool ForcedMinHook { get; set; } - public List ThirdRepo => new(); + public List ThirdRepo => []; public bool HasThirdRepo { get; set; } } diff --git a/Dalamud/Utility/ArrayExtensions.cs b/Dalamud/Utility/ArrayExtensions.cs index 5b6ce2332..5444468c6 100644 --- a/Dalamud/Utility/ArrayExtensions.cs +++ b/Dalamud/Utility/ArrayExtensions.cs @@ -115,8 +115,7 @@ internal static class ArrayExtensions if (count < 0 || startIndex > list.Count - count) throw new ArgumentOutOfRangeException(nameof(count), count, null); - if (match == null) - throw new ArgumentNullException(nameof(match)); + ArgumentNullException.ThrowIfNull(match); var endIndex = startIndex + count; for (var i = startIndex; i < endIndex; i++) @@ -138,8 +137,7 @@ internal static class ArrayExtensions /// public static int FindLastIndex(this IReadOnlyList list, int startIndex, int count, Predicate match) { - if (match == null) - throw new ArgumentNullException(nameof(match)); + ArgumentNullException.ThrowIfNull(match); if (list.Count == 0) { diff --git a/Dalamud/Utility/CultureFixes.cs b/Dalamud/Utility/CultureFixes.cs index 133e79c71..f37844206 100644 --- a/Dalamud/Utility/CultureFixes.cs +++ b/Dalamud/Utility/CultureFixes.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace Dalamud.Utility; @@ -22,7 +22,7 @@ internal static class CultureFixes // This glyph is not present in any game fonts and not in the range for our Noto // so it will be rendered as a geta (=) instead. That's a hack, but it works and // doesn't look as weird. - CultureInfo PatchCulture(CultureInfo info) + static CultureInfo PatchCulture(CultureInfo info) { var newCulture = (CultureInfo)info.Clone(); diff --git a/Dalamud/Utility/DateTimeSpanExtensions.cs b/Dalamud/Utility/DateTimeSpanExtensions.cs index 3cf8975af..d60b17924 100644 --- a/Dalamud/Utility/DateTimeSpanExtensions.cs +++ b/Dalamud/Utility/DateTimeSpanExtensions.cs @@ -100,7 +100,7 @@ public static class DateTimeSpanExtensions private sealed class ParsedRelativeFormatStrings { - private readonly List<(float MinSeconds, string FormatString)> formatStrings = new(); + private readonly List<(float MinSeconds, string FormatString)> formatStrings = []; public ParsedRelativeFormatStrings(string value) { diff --git a/Dalamud/Utility/DiagnosticUtil.cs b/Dalamud/Utility/DiagnosticUtil.cs index 155d5cda7..d853a2668 100644 --- a/Dalamud/Utility/DiagnosticUtil.cs +++ b/Dalamud/Utility/DiagnosticUtil.cs @@ -1,8 +1,6 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Linq; -using Dalamud.Bindings.ImGui; - namespace Dalamud.Utility; /// diff --git a/Dalamud/Utility/DisposeSafety.cs b/Dalamud/Utility/DisposeSafety.cs index 64d31048f..ac7ed07d7 100644 --- a/Dalamud/Utility/DisposeSafety.cs +++ b/Dalamud/Utility/DisposeSafety.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; @@ -92,7 +92,7 @@ public static class DisposeSafety new AggregateException( new[] { e }.Concat( (IEnumerable)r.Exception?.InnerExceptions - ?? new[] { new OperationCanceledException() }))); + ?? [new OperationCanceledException()]))); } } @@ -125,7 +125,7 @@ public static class DisposeSafety } catch (Exception de) { - exceptions ??= new(); + exceptions ??= []; exceptions.Add(de); } } @@ -140,7 +140,7 @@ public static class DisposeSafety /// public class ScopedFinalizer : IDisposeCallback, IAsyncDisposable { - private readonly List objects = new(); + private readonly List objects = []; /// public event Action? BeforeDispose; @@ -338,7 +338,7 @@ public static class DisposeSafety } catch (Exception ex) { - exceptions ??= new(); + exceptions ??= []; exceptions.Add(ex); } } @@ -402,7 +402,7 @@ public static class DisposeSafety } catch (Exception ex) { - exceptions ??= new(); + exceptions ??= []; exceptions.Add(ex); } } diff --git a/Dalamud/Utility/DynamicPriorityQueueLoader.cs b/Dalamud/Utility/DynamicPriorityQueueLoader.cs index 83fd366bb..c32858bb4 100644 --- a/Dalamud/Utility/DynamicPriorityQueueLoader.cs +++ b/Dalamud/Utility/DynamicPriorityQueueLoader.cs @@ -15,7 +15,7 @@ internal class DynamicPriorityQueueLoader : IDisposable private readonly Channel newItemChannel; private readonly Channel workTokenChannel; - private readonly List workItemPending = new(); + private readonly List workItemPending = []; private bool disposing; diff --git a/Dalamud/Utility/EventHandlerExtensions.cs b/Dalamud/Utility/EventHandlerExtensions.cs index 285e18fa2..415937dbf 100644 --- a/Dalamud/Utility/EventHandlerExtensions.cs +++ b/Dalamud/Utility/EventHandlerExtensions.cs @@ -4,6 +4,7 @@ using Dalamud.Game; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Game.Gui.NamePlate; using Dalamud.Plugin.Services; + using Serilog; namespace Dalamud.Utility; diff --git a/Dalamud/Utility/FuzzyMatcher.cs b/Dalamud/Utility/FuzzyMatcher.cs index 03723da89..d8b881300 100644 --- a/Dalamud/Utility/FuzzyMatcher.cs +++ b/Dalamud/Utility/FuzzyMatcher.cs @@ -1,4 +1,4 @@ -#define BORDER_MATCHING +#define BORDER_MATCHING using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -32,18 +32,12 @@ internal readonly ref struct FuzzyMatcher this.needleFinalPosition = this.needleSpan.Length - 1; this.mode = matchMode; - switch (matchMode) + this.needleSegments = matchMode switch { - case MatchMode.FuzzyParts: - this.needleSegments = FindNeedleSegments(this.needleSpan); - break; - case MatchMode.Fuzzy: - case MatchMode.Simple: - this.needleSegments = EmptySegArray; - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, null); - } + MatchMode.FuzzyParts => FindNeedleSegments(this.needleSpan), + MatchMode.Fuzzy or MatchMode.Simple => EmptySegArray, + _ => throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, null), + }; } private static (int Start, int End)[] FindNeedleSegments(ReadOnlySpan span) diff --git a/Dalamud/Utility/Hash.cs b/Dalamud/Utility/Hash.cs index 08fc78901..952c2305f 100644 --- a/Dalamud/Utility/Hash.cs +++ b/Dalamud/Utility/Hash.cs @@ -1,4 +1,4 @@ -using System.Security.Cryptography; +using System.Security.Cryptography; namespace Dalamud.Utility; @@ -24,10 +24,9 @@ public static class Hash /// The computed hash. internal static string GetSha256Hash(byte[] buffer) { - using var sha = SHA256.Create(); - var hash = sha.ComputeHash(buffer); + var hash = SHA256.HashData(buffer); return ByteArrayToString(hash); } - private static string ByteArrayToString(byte[] ba) => BitConverter.ToString(ba).Replace("-", string.Empty); + private static string ByteArrayToString(byte[] ba) => Convert.ToHexString(ba); } diff --git a/Dalamud/Utility/RollingList.cs b/Dalamud/Utility/RollingList.cs index 0f1553bf9..896b74fbf 100644 --- a/Dalamud/Utility/RollingList.cs +++ b/Dalamud/Utility/RollingList.cs @@ -40,7 +40,7 @@ namespace Dalamud.Utility { ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0); this.size = size; - this.items = new(); + this.items = []; } /// Initializes a new instance of the class. diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index cdf601852..e1a4dafc2 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -5,8 +5,8 @@ using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures.Wrappers; + using Serilog; namespace Dalamud.Utility.Signatures; @@ -72,9 +72,8 @@ internal static class SignatureHelper } } - IntPtr ptr; var success = sig.ScanType == ScanType.Text - ? scanner.TryScanText(sig.Signature, out ptr) + ? scanner.TryScanText(sig.Signature, out var ptr) : scanner.TryGetStaticAddressFromSig(sig.Signature, out ptr); if (!success) { @@ -159,7 +158,7 @@ internal static class SignatureHelper continue; } - var hook = creator.Invoke(null, new object?[] { ptr, detour, false }) as IDalamudHook; + var hook = creator.Invoke(null, [ptr, detour, false]) as IDalamudHook; info.SetValue(self, hook); createdHooks.Add(hook); diff --git a/Dalamud/Utility/StringExtensions.cs b/Dalamud/Utility/StringExtensions.cs index c28aebab2..7f9975f82 100644 --- a/Dalamud/Utility/StringExtensions.cs +++ b/Dalamud/Utility/StringExtensions.cs @@ -57,7 +57,7 @@ public static class StringExtensions /// The input string. /// /// A new string with the first character converted to uppercase. - [return: NotNullIfNotNull("input")] + [return: NotNullIfNotNull(nameof(input))] public static string? FirstCharToUpper(this string? input, CultureInfo? culture = null) => string.IsNullOrWhiteSpace(input) ? input @@ -69,7 +69,7 @@ public static class StringExtensions /// The input string. /// /// A new string with the first character converted to lowercase. - [return: NotNullIfNotNull("input")] + [return: NotNullIfNotNull(nameof(input))] public static string? FirstCharToLower(this string? input, CultureInfo? culture = null) => string.IsNullOrWhiteSpace(input) ? input diff --git a/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs index ec108403e..f952cba7e 100644 --- a/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs +++ b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs @@ -51,38 +51,22 @@ internal static unsafe partial class TerraFxComInterfaceExtensions throw new ArgumentOutOfRangeException(nameof(mode), mode, null); } - switch (access) + grfMode |= access switch { - case FileAccess.Read: - grfMode |= STGM.STGM_READ; - break; - case FileAccess.Write: - grfMode |= STGM.STGM_WRITE; - break; - case FileAccess.ReadWrite: - grfMode |= STGM.STGM_READWRITE; - break; - default: - throw new ArgumentOutOfRangeException(nameof(access), access, null); - } + FileAccess.Read => STGM.STGM_READ, + FileAccess.Write => STGM.STGM_WRITE, + FileAccess.ReadWrite => (uint)STGM.STGM_READWRITE, + _ => throw new ArgumentOutOfRangeException(nameof(access), access, null), + }; - switch (share) + grfMode |= share switch { - case FileShare.None: - grfMode |= STGM.STGM_SHARE_EXCLUSIVE; - break; - case FileShare.Read: - grfMode |= STGM.STGM_SHARE_DENY_WRITE; - break; - case FileShare.Write: - grfMode |= STGM.STGM_SHARE_DENY_READ; - break; - case FileShare.ReadWrite: - grfMode |= STGM.STGM_SHARE_DENY_NONE; - break; - default: - throw new NotSupportedException($"Only ${FileShare.Read} and ${FileShare.Write} are supported."); - } + FileShare.None => STGM.STGM_SHARE_EXCLUSIVE, + FileShare.Read => STGM.STGM_SHARE_DENY_WRITE, + FileShare.Write => STGM.STGM_SHARE_DENY_READ, + FileShare.ReadWrite => (uint)STGM.STGM_SHARE_DENY_NONE, + _ => throw new NotSupportedException($"Only ${FileShare.Read} and ${FileShare.Write} are supported."), + }; using var stream = default(ComPtr); fixed (char* pPath = path) diff --git a/Dalamud/Utility/Timing/Timings.cs b/Dalamud/Utility/Timing/Timings.cs index e2c00461c..563221fb9 100644 --- a/Dalamud/Utility/Timing/Timings.cs +++ b/Dalamud/Utility/Timing/Timings.cs @@ -19,12 +19,12 @@ public static class Timings /// /// All concluded timings. /// - internal static readonly SortedList AllTimings = new(); + internal static readonly SortedList AllTimings = []; /// /// List of all timing events. /// - internal static readonly List Events = new(); + internal static readonly List Events = []; private static readonly AsyncLocal>> TaskTimingHandleStorage = new(); @@ -36,7 +36,7 @@ public static class Timings get { if (TaskTimingHandleStorage.Value == null || TaskTimingHandleStorage.Value.Item1 != Task.CurrentId) - TaskTimingHandleStorage.Value = Tuple.Create>(Task.CurrentId, new()); + TaskTimingHandleStorage.Value = Tuple.Create>(Task.CurrentId, []); return TaskTimingHandleStorage.Value!.Item2!; } set => TaskTimingHandleStorage.Value = Tuple.Create(Task.CurrentId, value); @@ -53,7 +53,7 @@ public static class Timings var outerTimingHandle = TaskTimingHandles; return () => { - T res = default(T); + var res = default(T); var prev = TaskTimingHandles; TaskTimingHandles = outerTimingHandle; try diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 0ea5bbcbf..72db5cfc6 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -367,7 +367,7 @@ public static partial class Util /// Human readable version. public static string FormatBytes(long bytes) { - string[] suffix = { "B", "KB", "MB", "GB", "TB" }; + string[] suffix = ["B", "KB", "MB", "GB", "TB"]; int i; double dblSByte = bytes; for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) @@ -823,7 +823,7 @@ public static partial class Util MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, null, - new[] { typeof(object), typeof(IList), typeof(ulong) }, + [typeof(object), typeof(IList), typeof(ulong)], obj.GetType(), true); @@ -850,7 +850,7 @@ public static partial class Util ilg.Emit(OpCodes.Call, mm); ilg.Emit(OpCodes.Ret); - dm.Invoke(null, new[] { obj, path, addr }); + dm.Invoke(null, [obj, path, addr]); } #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type @@ -1029,8 +1029,8 @@ public static partial class Util foreach (var f in obj.GetType() .GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance)) { - var fixedBuffer = (FixedBufferAttribute)f.GetCustomAttribute(typeof(FixedBufferAttribute)); - var offset = (FieldOffsetAttribute)f.GetCustomAttribute(typeof(FieldOffsetAttribute)); + var fixedBuffer = f.GetCustomAttribute(); + var offset = f.GetCustomAttribute(); if (fixedBuffer != null) { diff --git a/Dalamud/Utility/WeakConcurrentCollection.cs b/Dalamud/Utility/WeakConcurrentCollection.cs index a3bc04651..c23ebac9a 100644 --- a/Dalamud/Utility/WeakConcurrentCollection.cs +++ b/Dalamud/Utility/WeakConcurrentCollection.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -11,7 +11,7 @@ namespace Dalamud.Utility; /// The type of object that we're tracking. public class WeakConcurrentCollection : ICollection where T : class { - private readonly ConditionalWeakTable cwt = new(); + private readonly ConditionalWeakTable cwt = []; /// public int Count => this.cwt.Count(); From 290ad9fc41aa784dd7be40f329dcdb13a8bc3aec Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 9 Jan 2026 10:48:58 +0100 Subject: [PATCH 409/477] Fix leaking colors in sheet redirects for Item --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index d16b3b9b3..58b9011e6 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -845,7 +845,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator using var rssb = new RentedSeStringBuilder(); var sb = rssb.Builder; - sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); + sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); // appends colortype and edgecolortype if (!skipLink) sb.PushLink(LinkMacroPayloadType.Item, itemId, rarity, 0u); // arg3 = some LogMessage flag based on LogKind RowId? => "89 5C 24 20 E8 ?? ?? ?? ?? 48 8B 1F" @@ -868,6 +868,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!skipLink) sb.PopLink(); + sb.PopEdgeColorType(); + sb.PopColorType(); + text = sb.ToReadOnlySeString(); } From 90c29e5646364106ee14fa58b0fbca6452c098ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 Jan 2026 18:40:18 +0000 Subject: [PATCH 410/477] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 52cb5e0a9..31e50c3f2 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 52cb5e0a9a7a1138d8c2406c277307a6c9ad8898 +Subproject commit 31e50c3f267dd845891b328140106a0cc3b1f35e From b29b7851d99ac09d3672f7d473656b33ad76f820 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 Jan 2026 18:40:20 +0000 Subject: [PATCH 411/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index b6f886afc..ae1917bf1 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b6f886afc2b1a54d8fd76c37a260a05f214a559e +Subproject commit ae1917bf103926bfd157c7d911efac58c0e28666 From f635673ce91e33412a3ed1bcc477567906787940 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 9 Jan 2026 12:51:08 -0800 Subject: [PATCH 412/477] Use AgentInterfacePtr --- Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs index b4a904dde..ef0f9021a 100644 --- a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs +++ b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Agent.AgentArgTypes; +using Dalamud.Game.NativeWrapper; + +namespace Dalamud.Game.Agent.AgentArgTypes; /// /// Base class for AgentLifecycle AgentArgTypes. @@ -15,7 +17,7 @@ public unsafe class AgentArgs /// /// Gets the pointer to the Agents AgentInterface*. /// - public nint Agent { get; internal set; } + public AgentInterfacePtr Agent { get; internal set; } /// /// Gets the agent id. @@ -33,5 +35,5 @@ public unsafe class AgentArgs /// AgentInterface. /// Typed pointer to contained Agents AgentInterface. public T* GetAgentPointer() where T : unmanaged - => (T*)this.Agent; + => (T*)this.Agent.Address; } From 6c8b2b4a6d4b10791e388a568f5bd11fec530c2f Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 9 Jan 2026 12:52:33 -0800 Subject: [PATCH 413/477] Remove casts --- Dalamud/Game/Agent/AgentVirtualTable.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Agent/AgentVirtualTable.cs b/Dalamud/Game/Agent/AgentVirtualTable.cs index 3c23616e8..e00f9e433 100644 --- a/Dalamud/Game/Agent/AgentVirtualTable.cs +++ b/Dalamud/Game/Agent/AgentVirtualTable.cs @@ -125,7 +125,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.receiveEventArgs.Agent = (nint)thisPtr; + this.receiveEventArgs.Agent = thisPtr; this.receiveEventArgs.AgentId = this.agentId; this.receiveEventArgs.ReturnValue = (nint)returnValue; this.receiveEventArgs.AtkValues = (nint)values; @@ -166,7 +166,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.filteredReceiveEventArgs.Agent = (nint)thisPtr; + this.filteredReceiveEventArgs.Agent = thisPtr; this.filteredReceiveEventArgs.AgentId = this.agentId; this.filteredReceiveEventArgs.ReturnValue = (nint)returnValue; this.filteredReceiveEventArgs.AtkValues = (nint)values; @@ -205,7 +205,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.showArgs.Agent = (nint)thisPtr; + this.showArgs.Agent = thisPtr; this.showArgs.AgentId = this.agentId; this.lifecycleService.InvokeListenersSafely(AgentEvent.PreShow, this.showArgs); @@ -233,7 +233,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.hideArgs.Agent = (nint)thisPtr; + this.hideArgs.Agent = thisPtr; this.hideArgs.AgentId = this.agentId; this.lifecycleService.InvokeListenersSafely(AgentEvent.PreHide, this.hideArgs); @@ -261,7 +261,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.updateArgs.Agent = (nint)thisPtr; + this.updateArgs.Agent = thisPtr; this.updateArgs.AgentId = this.agentId; this.lifecycleService.InvokeListenersSafely(AgentEvent.PreUpdate, this.updateArgs); @@ -289,7 +289,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.gameEventArgs.Agent = (nint)thisPtr; + this.gameEventArgs.Agent = thisPtr; this.gameEventArgs.AgentId = this.agentId; this.gameEventArgs.GameEvent = (int)gameEvent; @@ -320,7 +320,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.levelChangeArgs.Agent = (nint)thisPtr; + this.levelChangeArgs.Agent = thisPtr; this.levelChangeArgs.AgentId = this.agentId; this.levelChangeArgs.ClassJobId = classJobId; this.levelChangeArgs.Level = level; @@ -353,7 +353,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.classJobChangeArgs.Agent = (nint)thisPtr; + this.classJobChangeArgs.Agent = thisPtr; this.classJobChangeArgs.AgentId = this.agentId; this.classJobChangeArgs.ClassJobId = classJobId; From b2fb6949d2b9bafefcd35c1ac7dcbb9d7321539c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 06:37:41 +0000 Subject: [PATCH 414/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index ae1917bf1..d83e0c13d 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit ae1917bf103926bfd157c7d911efac58c0e28666 +Subproject commit d83e0c13d3c802d4a483f373edcd129bc4802073 From fab7eef244e17a66c57caf1131709f501cf3c7f7 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 10 Jan 2026 14:25:22 +0100 Subject: [PATCH 415/477] Update UIColorWidget.cs --- Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index 9baf2848a..dc1ab4e30 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Text; using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility.Raii; using Dalamud.Data; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; From 8bb6cdd8d6a0e524cb2355f22ba1001d5f176139 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 Jan 2026 16:57:18 +0100 Subject: [PATCH 416/477] Add "enum cloning" source generator --- Dalamud.sln | 23 +++ Dalamud/Dalamud.csproj | 9 + Dalamud/EnumCloneMap.txt | 3 + .../Dalamud.EnumGenerator.Sample.csproj | 20 ++ .../EnumCloneMap.txt | 4 + .../SourceEnums.cs | 9 + .../Dalamud.EnumGenerator.Tests.csproj | 29 +++ .../EnumCloneMapTests.cs | 47 +++++ .../Utils/TestAdditionalFile.cs | 21 ++ .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 9 + .../Dalamud.EnumGenerator.csproj | 33 ++++ .../EnumCloneGenerator.cs | 181 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 4 + 14 files changed, 395 insertions(+) create mode 100644 Dalamud/EnumCloneMap.txt create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs create mode 100644 generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs diff --git a/Dalamud.sln b/Dalamud.sln index de91e7ceb..fa26a5d67 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -75,6 +75,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "l EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source Generators", "Source Generators", "{50BEC23B-FFFD-427B-A95D-27E1D1958FFF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj", "{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Sample", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Sample\Dalamud.EnumGenerator.Sample.csproj", "{8CDAEB2D-5022-450A-A97F-181C6270185F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Tests", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Tests\Dalamud.EnumGenerator.Tests.csproj", "{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -173,6 +181,18 @@ Global {88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.Build.0 = Debug|Any CPU {88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.ActiveCfg = Release|Any CPU {88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = Release|Any CPU + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.ActiveCfg = Debug|x64 + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.Build.0 = Debug|x64 + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.ActiveCfg = Release|x64 + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.Build.0 = Release|x64 + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.Build.0 = Debug|x64 + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.ActiveCfg = Release|x64 + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.Build.0 = Release|x64 + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.ActiveCfg = Debug|x64 + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.Build.0 = Debug|x64 + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.ActiveCfg = Release|x64 + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -197,6 +217,9 @@ Global {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {88FB719B-EB41-73C5-8D25-C03E0C69904F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF} + {8CDAEB2D-5022-450A-A97F-181C6270185F} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF} + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index f5e75af63..bb8f5af7c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -88,6 +88,15 @@ + + + + + + + + + imgui-frag.hlsl.bytes diff --git a/Dalamud/EnumCloneMap.txt b/Dalamud/EnumCloneMap.txt new file mode 100644 index 000000000..bbc3c1eda --- /dev/null +++ b/Dalamud/EnumCloneMap.txt @@ -0,0 +1,3 @@ +# Format: Target.Full.TypeName = Source.Full.EnumTypeName +# Example: Generate a local enum MyGeneratedEnum in namespace Sample.Gen mapped to SourceEnums.SampleSourceEnum +Dalamud.Game.Agent.AgentId = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj new file mode 100644 index 000000000..225ea5f94 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj @@ -0,0 +1,20 @@ + + + + net9.0 + enable + Dalamud.EnumGenerator.Sample + + false + + + + + + + + + + + + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt new file mode 100644 index 000000000..a7db08bf3 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt @@ -0,0 +1,4 @@ +# Format: Target.Full.TypeName = Source.Full.EnumTypeName +# Example: Generate a local enum MyGeneratedEnum in namespace Sample.Gen mapped to SourceEnums.SampleSourceEnum +Dalamud.EnumGenerator.Sample.Gen.MyGeneratedEnum = Dalamud.EnumGenerator.Sample.SourceEnums.SampleSourceEnum + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs new file mode 100644 index 000000000..407b4c151 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs @@ -0,0 +1,9 @@ +namespace Dalamud.EnumGenerator.Sample.SourceEnums +{ + public enum SampleSourceEnum : long + { + First = 1, + Second = 2, + Third = 10000000000L + } +} diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj new file mode 100644 index 000000000..50de4a7c8 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj @@ -0,0 +1,29 @@ + + + + net9.0 + enable + + false + + Dalamud.EnumGenerator.Tests + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs new file mode 100644 index 000000000..f14279c53 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs @@ -0,0 +1,47 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Dalamud.EnumGenerator.Tests; + +public class EnumCloneMapTests +{ + [Fact] + public void ParseMappings_SimpleLines_ParsesCorrectly() + { + var text = @"# Comment line +My.Namespace.Target = Other.Namespace.Source + +Another.Target = Some.Source"; + + var results = Dalamud.EnumGenerator.EnumCloneGenerator.ParseMappings(text); + + Assert.Equal(2, results.Length); + Assert.Equal("My.Namespace.Target", results[0].TargetFullName); + Assert.Equal("Other.Namespace.Source", results[0].SourceFullName); + Assert.Equal("Another.Target", results[1].TargetFullName); + } + + [Fact] + public void Generator_ProducesFile_WhenSourceResolved() + { + // We'll create a compilation that contains a source enum type and add an AdditionalText mapping + var sourceEnum = @"namespace Foo.Bar { public enum SourceEnum { A = 1, B = 2 } }"; + + var mapText = "GeneratedNs.TargetEnum = Foo.Bar.SourceEnum"; + + var generator = new EnumCloneGenerator(); + var driver = CSharpGeneratorDriver.Create(generator) + .AddAdditionalTexts(ImmutableArray.Create(new Utils.TestAdditionalFile("EnumCloneMap.txt", mapText))); + + var compilation = CSharpCompilation.Create("TestGen", [CSharpSyntaxTree.ParseText(sourceEnum)], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics); + + var generated = newCompilation.SyntaxTrees.Select(t => t.FilePath).Where(p => p.EndsWith("TargetEnum.CloneEnum.g.cs")).ToArray(); + Assert.Single(generated); + } +} diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs new file mode 100644 index 000000000..e5c0df848 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs @@ -0,0 +1,21 @@ +using System.Threading; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Dalamud.EnumGenerator.Tests.Utils; + +public class TestAdditionalFile : AdditionalText +{ + private readonly SourceText text; + + public TestAdditionalFile(string path, string text) + { + Path = path; + this.text = SourceText.From(text); + } + + public override SourceText GetText(CancellationToken cancellationToken = new()) => this.text; + + public override string Path { get; } +} diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..60b59dd99 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..e90084796 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,9 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +ENUMGEN001 | EnumGenerator | Warning | SourceGeneratorWithAttributes +ENUMGEN002 | EnumGenerator | Warning | SourceGeneratorWithAttributes \ No newline at end of file diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj new file mode 100644 index 000000000..106b036a8 --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj @@ -0,0 +1,33 @@ + + + + netstandard2.0 + false + enable + latest + + true + true + + Dalamud.EnumGenerator + Dalamud.EnumGenerator + + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs new file mode 100644 index 000000000..95af4c38b --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Globalization; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Dalamud.EnumGenerator; + +[Generator] +public class EnumCloneGenerator : IIncrementalGenerator +{ + private const string NewLine = "\r\n"; + + private const string MappingFileName = "EnumCloneMap.txt"; + + private static readonly DiagnosticDescriptor MissingSourceDescriptor = new( + id: "ENUMGEN001", + title: "Source enum not found", + messageFormat: "Source enum '{0}' could not be resolved by the compilation", + category: "EnumGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor DuplicateTargetDescriptor = new( + id: "ENUMGEN002", + title: "Duplicate target mapping", + messageFormat: "Target enum '{0}' is mapped multiple times; generation skipped for this target", + category: "EnumGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Read mappings from additional files named EnumCloneMap.txt + var mappingEntries = context.AdditionalTextsProvider + .Where(at => Path.GetFileName(at.Path).Equals(MappingFileName, StringComparison.OrdinalIgnoreCase)) + .SelectMany((at, _) => ParseMappings(at.GetText()?.ToString() ?? string.Empty)); + + // Combine with compilation so we can resolve types + var compilationAndMaps = context.CompilationProvider.Combine(mappingEntries.Collect()); + + context.RegisterSourceOutput(compilationAndMaps, (spc, pair) => + { + var compilation = pair.Left; + var maps = pair.Right; + + // Detect duplicate targets first and report diagnostics + var duplicateTargets = maps.GroupBy(m => m.TargetFullName, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToImmutableArray(); + foreach (var dup in duplicateTargets) + { + var diag = Diagnostic.Create(DuplicateTargetDescriptor, Location.None, dup); + spc.ReportDiagnostic(diag); + } + + foreach (var (targetFullName, sourceFullName) in maps) + { + if (string.IsNullOrWhiteSpace(targetFullName) || string.IsNullOrWhiteSpace(sourceFullName)) + continue; + + if (duplicateTargets.Contains(targetFullName, StringComparer.OrdinalIgnoreCase)) + continue; + + // Resolve the source enum type by metadata name (namespace.type) + var sourceSymbol = compilation.GetTypeByMetadataName(sourceFullName); + if (sourceSymbol is null) + { + // Report diagnostic for missing source type + var diag = Diagnostic.Create(MissingSourceDescriptor, Location.None, sourceFullName); + spc.ReportDiagnostic(diag); + continue; + } + + if (sourceSymbol.TypeKind != TypeKind.Enum) + continue; + + var sourceNamed = sourceSymbol; // GetTypeByMetadataName already returns INamedTypeSymbol + + // Split target into namespace and type name + string? targetNamespace = null; + var targetName = targetFullName; + var lastDot = targetFullName.LastIndexOf('.'); + if (lastDot >= 0) + { + targetNamespace = targetFullName.Substring(0, lastDot); + targetName = targetFullName.Substring(lastDot + 1); + } + + var underlyingType = sourceNamed.EnumUnderlyingType; + var underlyingDisplay = underlyingType?.ToDisplayString() ?? "int"; + + var fields = sourceNamed.GetMembers() + .OfType() + .Where(f => f.IsStatic && f.HasConstantValue) + .ToArray(); + + var memberLines = fields.Select(f => + { + var name = f.Name; + var constValue = f.ConstantValue; + string literal; + + var st = underlyingType?.SpecialType ?? SpecialType.System_Int32; + + if (constValue is null) + { + literal = "0"; + } + else if (st == SpecialType.System_UInt64) + { + literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) + "UL"; + } + else if (st == SpecialType.System_UInt32) + { + literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) + "U"; + } + else if (st == SpecialType.System_Int64) + { + literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) + "L"; + } + else + { + literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) ?? throw new InvalidOperationException("Unable to convert enum constant value to string."); + } + + return $" {name} = {literal},"; + }); + + var membersText = string.Join(NewLine, memberLines); + + var nsPrefix = targetNamespace is null ? string.Empty : $"namespace {targetNamespace};" + NewLine + NewLine; + + var code = "// " + NewLine + NewLine + + nsPrefix + + $"public enum {targetName} : {underlyingDisplay}" + NewLine + + "{" + NewLine + + membersText + NewLine + + "}" + NewLine; + + var hintName = $"{targetName}.CloneEnum.g.cs"; + spc.AddSource(hintName, SourceText.From(code, Encoding.UTF8)); + } + }); + } + + internal static ImmutableArray<(string TargetFullName, string SourceFullName)> ParseMappings(string text) + { + var builder = ImmutableArray.CreateBuilder<(string, string)>(); + using var reader = new StringReader(text); + string? line; + while ((line = reader.ReadLine()) != null) + { + // Remove comments starting with # + var commentIndex = line.IndexOf('#'); + var content = commentIndex >= 0 ? line.Substring(0, commentIndex) : line; + content = content.Trim(); + if (string.IsNullOrEmpty(content)) + continue; + + // Expected format: Target.Full.Name = Source.Full.Name + var idx = content.IndexOf('='); + if (idx <= 0) + continue; + + var left = content.Substring(0, idx).Trim(); + var right = content.Substring(idx + 1).Trim(); + if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right)) + continue; + + builder.Add((left, right)); + } + + return builder.ToImmutable(); + } +} diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6eac4d12e --- /dev/null +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dalamud.EnumGenerator.Tests")] + From dd94d107225e6fa0d652d52a813383c394c09cea Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 Jan 2026 17:09:51 +0100 Subject: [PATCH 417/477] Add conversion extension method from source enum --- .../Dalamud.EnumGenerator/EnumCloneGenerator.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs index 95af4c38b..10cf0723c 100644 --- a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs +++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs @@ -136,12 +136,24 @@ public class EnumCloneGenerator : IIncrementalGenerator var nsPrefix = targetNamespace is null ? string.Empty : $"namespace {targetNamespace};" + NewLine + NewLine; + var sourceFullyQualified = sourceNamed.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var code = "// " + NewLine + NewLine + nsPrefix + $"public enum {targetName} : {underlyingDisplay}" + NewLine + "{" + NewLine + membersText + NewLine - + "}" + NewLine; + + "}" + NewLine + NewLine; + + var extClassName = targetName + "Conversions"; + var extMethodName = "ToDalamud" + targetName; + + var extClass = $"public static class {extClassName}" + NewLine + + "{" + NewLine + + $" public static {targetName} {extMethodName}(this {sourceFullyQualified} value) => ({targetName})(({underlyingDisplay})value);" + NewLine + + "}" + NewLine; + + code += extClass; var hintName = $"{targetName}.CloneEnum.g.cs"; spc.AddSource(hintName, SourceText.From(code, Encoding.UTF8)); From 0c2ce097ed2243b304f06207d9cc9692be43de0c Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sat, 10 Jan 2026 08:30:15 -0800 Subject: [PATCH 418/477] Use generated AgentId --- Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs | 2 +- Dalamud/Game/Agent/AgentLifecycle.cs | 20 +++++++++---------- .../Game/Agent/AgentLifecycleEventListener.cs | 4 ++-- Dalamud/Game/Agent/AgentVirtualTable.cs | 10 +++++----- Dalamud/Plugin/Services/IAgentLifecycle.cs | 8 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs index ef0f9021a..1de80694f 100644 --- a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs +++ b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs @@ -22,7 +22,7 @@ public unsafe class AgentArgs /// /// Gets the agent id. /// - public uint AgentId { get; internal set; } + public AgentId AgentId { get; internal set; } /// /// Gets the type of these args. diff --git a/Dalamud/Game/Agent/AgentLifecycle.cs b/Dalamud/Game/Agent/AgentLifecycle.cs index 1306a92c1..75ed47d86 100644 --- a/Dalamud/Game/Agent/AgentLifecycle.cs +++ b/Dalamud/Game/Agent/AgentLifecycle.cs @@ -57,7 +57,7 @@ internal unsafe class AgentLifecycle : IInternalDisposableService /// Gets a list of all AgentLifecycle Event Listeners. ///
/// Mapping is: EventType -> ListenerList - internal Dictionary>> EventListeners { get; } = []; + internal Dictionary>> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -128,7 +128,7 @@ internal unsafe class AgentLifecycle : IInternalDisposableService if (!this.EventListeners.TryGetValue(eventType, out var agentListeners)) return; // Handle listeners for this event type that don't care which agent is triggering it - if (agentListeners.TryGetValue(uint.MaxValue, out var globalListeners)) + if (agentListeners.TryGetValue((AgentId)uint.MaxValue, out var globalListeners)) { foreach (var listener in globalListeners) { @@ -154,7 +154,7 @@ internal unsafe class AgentLifecycle : IInternalDisposableService } catch (Exception e) { - Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific agent {(AgentId)args.AgentId}."); + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific agent {args.AgentId}."); } } } @@ -208,7 +208,7 @@ internal unsafe class AgentLifecycle : IInternalDisposableService } // AgentVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions - AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, index, this)); + AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, (AgentId)index, this)); } catch (Exception e) { @@ -243,7 +243,7 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi } /// - public void RegisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate handler) + public void RegisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate handler) { foreach (var agentId in agentIds) { @@ -252,7 +252,7 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi } /// - public void RegisterListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate handler) + public void RegisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate handler) { var listener = new AgentLifecycleEventListener(eventType, agentId, handler); this.eventListeners.Add(listener); @@ -262,11 +262,11 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi /// public void RegisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate handler) { - this.RegisterListener(eventType, uint.MaxValue, handler); + this.RegisterListener(eventType, (AgentId)uint.MaxValue, handler); } /// - public void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate? handler = null) + public void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate? handler = null) { foreach (var agentId in agentIds) { @@ -275,7 +275,7 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi } /// - public void UnregisterListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate? handler = null) + public void UnregisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate? handler = null) { this.eventListeners.RemoveAll(entry => { @@ -291,7 +291,7 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi /// public void UnregisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate? handler = null) { - this.UnregisterListener(eventType, uint.MaxValue, handler); + this.UnregisterListener(eventType, (AgentId)uint.MaxValue, handler); } /// diff --git a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs index 3521d2c13..91f8aa3d3 100644 --- a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs +++ b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs @@ -13,7 +13,7 @@ public class AgentLifecycleEventListener /// Event type to listen for. /// Agent id to listen for. /// Delegate to invoke. - internal AgentLifecycleEventListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate functionDelegate) + internal AgentLifecycleEventListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate functionDelegate) { this.EventType = eventType; this.AgentId = agentId; @@ -24,7 +24,7 @@ public class AgentLifecycleEventListener /// Gets the agentId of the agent this listener is looking for. /// uint.MaxValue if it wants to be called for any agent. ///
- public uint AgentId { get; init; } + public AgentId AgentId { get; init; } /// /// Gets the event type this listener is looking for. diff --git a/Dalamud/Game/Agent/AgentVirtualTable.cs b/Dalamud/Game/Agent/AgentVirtualTable.cs index e00f9e433..e7f9a2f6e 100644 --- a/Dalamud/Game/Agent/AgentVirtualTable.cs +++ b/Dalamud/Game/Agent/AgentVirtualTable.cs @@ -27,7 +27,7 @@ internal unsafe class AgentVirtualTable : IDisposable private readonly AgentLifecycle lifecycleService; - private readonly uint agentId; + private readonly AgentId agentId; // Each agent gets its own set of args that are used to mutate the original call when used in pre-calls private readonly AgentReceiveEventArgs receiveEventArgs = new(); @@ -58,9 +58,9 @@ internal unsafe class AgentVirtualTable : IDisposable /// AgentInterface* for the agent to replace the table of. /// Agent ID. /// Reference to AgentLifecycle service to callback and invoke listeners. - internal AgentVirtualTable(AgentInterface* agent, uint agentId, AgentLifecycle lifecycleService) + internal AgentVirtualTable(AgentInterface* agent, AgentId agentId, AgentLifecycle lifecycleService) { - Log.Debug($"Initializing AgentVirtualTable for {(AgentId)agentId}, Address: {(nint)agent:X}"); + Log.Debug($"Initializing AgentVirtualTable for {agentId}, Address: {(nint)agent:X}"); this.agentInterface = agent; this.agentId = agentId; @@ -384,10 +384,10 @@ internal unsafe class AgentVirtualTable : IDisposable if (loggingEnabled) { // Manually disable the really spammy log events, you can comment this out if you need to debug them. - if (caller is "OnAgentUpdate" || (AgentId)this.agentId is AgentId.PadMouseMode) + if (caller is "OnAgentUpdate" || this.agentId is AgentId.PadMouseMode) return; - Log.Debug($"[{caller}]: {(AgentId)this.agentId}"); + Log.Debug($"[{caller}]: {this.agentId}"); } } } diff --git a/Dalamud/Plugin/Services/IAgentLifecycle.cs b/Dalamud/Plugin/Services/IAgentLifecycle.cs index a1ed26125..62178408d 100644 --- a/Dalamud/Plugin/Services/IAgentLifecycle.cs +++ b/Dalamud/Plugin/Services/IAgentLifecycle.cs @@ -24,7 +24,7 @@ public interface IAgentLifecycle : IDalamudService /// Event type to trigger on. /// Agent IDs that will trigger the handler to be invoked. /// The handler to invoke. - void RegisterListener(AgentEvent eventType, IEnumerable agentIds, AgentEventDelegate handler); + void RegisterListener(AgentEvent eventType, IEnumerable agentIds, AgentEventDelegate handler); /// /// Register a listener that will trigger on the specified event only for the specified agent. @@ -32,7 +32,7 @@ public interface IAgentLifecycle : IDalamudService /// Event type to trigger on. /// The agent ID that will trigger the handler to be invoked. /// The handler to invoke. - void RegisterListener(AgentEvent eventType, uint agentId, AgentEventDelegate handler); + void RegisterListener(AgentEvent eventType, AgentId agentId, AgentEventDelegate handler); /// /// Register a listener that will trigger on the specified event for any agent. @@ -50,7 +50,7 @@ public interface IAgentLifecycle : IDalamudService /// Event type to deregister. /// Agent IDs to deregister. /// Optional specific handler to remove. - void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, [Optional] AgentEventDelegate handler); + void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, [Optional] AgentEventDelegate handler); /// /// Unregister all listeners for the specified event type and agent ID. @@ -61,7 +61,7 @@ public interface IAgentLifecycle : IDalamudService /// Event type to deregister. /// Agent id to deregister. /// Optional specific handler to remove. - void UnregisterListener(AgentEvent eventType, uint agentId, [Optional] AgentEventDelegate handler); + void UnregisterListener(AgentEvent eventType, AgentId agentId, [Optional] AgentEventDelegate handler); /// /// Unregister an event type handler.
This will only remove a handler that is added via . From c545205e66d6e1baaa17083d414dbffa4a7f76c6 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 Jan 2026 17:53:49 +0100 Subject: [PATCH 419/477] Remove analyzer for source generator projects --- Dalamud.sln | 3 +++ generators/Directory.Build.props | 5 +++++ 2 files changed, 8 insertions(+) create mode 100644 generators/Directory.Build.props diff --git a/Dalamud.sln b/Dalamud.sln index fa26a5d67..3b1c4fd91 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -76,6 +76,9 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source Generators", "Source Generators", "{50BEC23B-FFFD-427B-A95D-27E1D1958FFF}" + ProjectSection(SolutionItems) = preProject + generators\Directory.Build.props = generators\Directory.Build.props + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj", "{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}" EndProject diff --git a/generators/Directory.Build.props b/generators/Directory.Build.props new file mode 100644 index 000000000..f699838f7 --- /dev/null +++ b/generators/Directory.Build.props @@ -0,0 +1,5 @@ + + + + + From 745b3a49396b476bc09f66e80cbf28fc70f53aac Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 11 Jan 2026 00:49:39 +0100 Subject: [PATCH 420/477] Remove ExperimentalAttribute from IUnlockState --- Dalamud/Game/UnlockState/UnlockState.cs | 2 -- Dalamud/Plugin/Services/IUnlockState.cs | 1 - 2 files changed, 3 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index cc70a524c..939548803 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -22,8 +22,6 @@ using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; namespace Dalamud.Game.UnlockState; -#pragma warning disable Dalamud001 - /// /// This class provides unlock state of various content in the game. /// diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 0409843c4..6703ece2e 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -10,7 +10,6 @@ namespace Dalamud.Plugin.Services; /// /// Interface for determining unlock state of various content in the game. /// -[Experimental("Dalamud001")] public interface IUnlockState : IDalamudService { /// From c1df0da9beaec38c7132e519c60c04e56ec0caef Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 11 Jan 2026 13:18:32 +0100 Subject: [PATCH 421/477] build: 14.0.1.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index bb8f5af7c..287bc5322 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 14.0.0.3 + 14.0.1.0 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 3b8f0bc92fa4a612a009d46cfa8cbd3c9bed41a2 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 25 Jan 2026 05:03:03 +0100 Subject: [PATCH 422/477] Update ClientStructs (#2579) 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 d83e0c13d..127047085 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit d83e0c13d3c802d4a483f373edcd129bc4802073 +Subproject commit 1270470855d6ac2d2f726b07019e21644c5658ec From b601bfdbfb72c33c5c298fe8146c3a0a7782a259 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 25 Jan 2026 05:03:57 +0100 Subject: [PATCH 423/477] Add Quest and Leve support to IUnlockState (#2581) * Fix AgentInterfacePtr.FocusAddon * Add Quest support to IUnlockState * Reorder, bail out early in Recipe and McGuffin * Add Leve support to IUnlockState * Fix warning * Disable log spam --- .../Game/NativeWrapper/AgentInterfacePtr.cs | 2 +- Dalamud/Game/UnlockState/UnlockState.cs | 48 +++++++++++++++++-- .../Windows/Data/Widgets/UIColorWidget.cs | 2 +- Dalamud/Plugin/Services/IUnlockState.cs | 14 ++++++ 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/NativeWrapper/AgentInterfacePtr.cs b/Dalamud/Game/NativeWrapper/AgentInterfacePtr.cs index b5a6375a9..b5e8938dd 100644 --- a/Dalamud/Game/NativeWrapper/AgentInterfacePtr.cs +++ b/Dalamud/Game/NativeWrapper/AgentInterfacePtr.cs @@ -81,7 +81,7 @@ public readonly unsafe struct AgentInterfacePtr(nint address) : IEquatable /// true when the addon was focused, false otherwise. - public readonly bool FocusAddon() => this.IsNull && this.Struct->FocusAddon(); + public readonly bool FocusAddon() => !this.IsNull && this.Struct->FocusAddon(); /// Determines whether the specified AgentInterfacePtr is equal to the current AgentInterfacePtr. /// The AgentInterfacePtr to compare with the current AgentInterfacePtr. diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 939548803..5ccd7fadb 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -311,9 +311,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState } /// - public bool IsMcGuffinUnlocked(McGuffin row) + public bool IsLeveCompleted(Leve row) { - return PlayerState.Instance()->IsMcGuffinUnlocked(row.RowId); + if (!this.IsLoaded) + return false; + + return QuestManager.Instance()->IsLevequestComplete((ushort)row.RowId); } /// @@ -328,6 +331,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return this.IsUnlockLinkUnlocked(row.UnlockLink); } + /// + public bool IsMcGuffinUnlocked(McGuffin row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsMcGuffinUnlocked(row.RowId); + } + /// public bool IsMountUnlocked(Mount row) { @@ -376,9 +388,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return UIState.IsPublicContentUnlocked(row.RowId); } + /// + public bool IsQuestCompleted(Quest row) + { + if (!this.IsLoaded) + return false; + + return QuestManager.IsQuestComplete(row.RowId); + } + /// public bool IsRecipeUnlocked(Recipe row) { + if (!this.IsLoaded) + return false; + return this.recipeData.IsRecipeUnlocked(row); } @@ -509,6 +533,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (rowRef.TryGetValue(out var itemRow)) return this.IsItemUnlocked(itemRow); + if (rowRef.TryGetValue(out var leveRow)) + return this.IsLeveCompleted(leveRow); + if (rowRef.TryGetValue(out var mjiLandmarkRow)) return this.IsMJILandmarkUnlocked(mjiLandmarkRow); @@ -536,6 +563,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (rowRef.TryGetValue(out var publicContentRow)) return this.IsPublicContentUnlocked(publicContentRow); + if (rowRef.TryGetValue(out var questRow)) + return this.IsQuestCompleted(questRow); + if (rowRef.TryGetValue(out var recipeRow)) return this.IsRecipeUnlocked(recipeRow); @@ -596,6 +626,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (!this.IsLoaded) return; + Log.Verbose("Checking for new unlocks..."); + this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); @@ -629,6 +661,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); + this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); @@ -637,6 +670,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // Not implemented: // - DescriptionPage: quite complex // - QuestAcceptAdditionCondition: ignored + // - Leve: AgentUpdateFlag.UnlocksUpdate is not set and the completed status can be unset again! // For some other day: // - FishingSpot @@ -676,7 +710,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState unlockedRowIds.Add(row.RowId); - Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}"); + // Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}"); foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) { @@ -796,7 +830,7 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat public bool IsItemUnlocked(Item row) => this.unlockStateService.IsItemUnlocked(row); /// - public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row); + public bool IsLeveCompleted(Leve row) => this.unlockStateService.IsLeveCompleted(row); /// public bool IsMJILandmarkUnlocked(MJILandmark row) => this.unlockStateService.IsMJILandmarkUnlocked(row); @@ -804,6 +838,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// public bool IsMKDLoreUnlocked(MKDLore row) => this.unlockStateService.IsMKDLoreUnlocked(row); + /// + public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row); + /// public bool IsMountUnlocked(Mount row) => this.unlockStateService.IsMountUnlocked(row); @@ -822,6 +859,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// public bool IsPublicContentUnlocked(PublicContentSheet row) => this.unlockStateService.IsPublicContentUnlocked(row); + /// + public bool IsQuestCompleted(Quest row) => this.unlockStateService.IsQuestCompleted(row); + /// public bool IsRecipeUnlocked(Recipe row) => this.unlockStateService.IsRecipeUnlocked(row); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index dc1ab4e30..bc6e5376c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -3,11 +3,11 @@ using System.Numerics; using System.Text; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Utility.Raii; using Dalamud.Data; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.ImGuiSeStringRenderer.Internal; +using Dalamud.Interface.Utility.Raii; using Lumina.Excel.Sheets; diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 6703ece2e..f51222ba1 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -205,6 +205,13 @@ public interface IUnlockState : IDalamudService /// if unlocked; otherwise, . bool IsItemUnlocked(Item row); + /// + /// Determines whether the specified Leve is completed. + /// + /// The Leve row to check. + /// if completed; otherwise, . + bool IsLeveCompleted(Leve row); + /// /// Determines whether the specified McGuffin is unlocked. /// @@ -268,6 +275,13 @@ public interface IUnlockState : IDalamudService /// if unlocked; otherwise, . bool IsPublicContentUnlocked(PublicContent row); + /// + /// Determines whether the specified Quest is completed. + /// + /// The Quest row to check. + /// if completed; otherwise, . + bool IsQuestCompleted(Quest row); + /// /// Determines whether the specified Recipe is unlocked. /// From 61423f1791b04fb561b91fef67c4decc5e4200a5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 25 Jan 2026 05:06:25 +0100 Subject: [PATCH 424/477] Fix being unable to edit TitleBgCollapsed (#2589) Fixes #999 --- Dalamud/Interface/Windowing/Window.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 3cbd1a521..dab3506c0 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -11,6 +11,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Windows.StyleEditor; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; @@ -461,7 +462,7 @@ public abstract class Window ImGuiHelpers.ForceNextWindowMainViewport(); var wasFocused = this.IsFocused; - if (wasFocused) + if (wasFocused && this is not StyleEditorWindow) { var style = ImGui.GetStyle(); var focusedHeaderColor = style.Colors[(int)ImGuiCol.TitleBgActive]; @@ -616,7 +617,7 @@ public abstract class Window this.DrawTitleBarButtons(); } - if (wasFocused) + if (wasFocused && this is not StyleEditorWindow) { ImGui.PopStyleColor(); } From 951290cac7f27ca67b76391649c5d9f0c38929bf Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 25 Jan 2026 05:06:55 +0100 Subject: [PATCH 425/477] Fix EnumGenerator configuration mapping (#2590) --- Dalamud.sln | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Dalamud.sln b/Dalamud.sln index 3b1c4fd91..758253b9c 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -184,18 +184,18 @@ Global {88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.Build.0 = Debug|Any CPU {88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.ActiveCfg = Release|Any CPU {88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = Release|Any CPU - {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.ActiveCfg = Debug|x64 - {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.Build.0 = Debug|x64 - {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.ActiveCfg = Release|x64 - {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.Build.0 = Release|x64 - {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.ActiveCfg = Debug|x64 - {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.Build.0 = Debug|x64 - {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.ActiveCfg = Release|x64 - {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.Build.0 = Release|x64 - {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.ActiveCfg = Debug|x64 - {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.Build.0 = Debug|x64 - {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.ActiveCfg = Release|x64 - {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.Build.0 = Release|x64 + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.Build.0 = Release|Any CPU + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.Build.0 = Release|Any CPU + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From ac7c4e889a853c61a7b08b71cfb73e57000a304e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 25 Jan 2026 12:40:51 +0100 Subject: [PATCH 426/477] Write troubleshooting to json file --- Dalamud/Support/Troubleshooting.cs | 8 ++++++++ DalamudCrashHandler/DalamudCrashHandler.cpp | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index 2dd0fb623..e94252027 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -85,6 +86,13 @@ public static class Troubleshooting var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload))); Log.Information($"TROUBLESHOOTING:{encodedPayload}"); + + File.WriteAllText( + Path.Join( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "XIVLauncher", + "dalamud.troubleshooting.json"), + JsonConvert.SerializeObject(payload, Formatting.Indented)); } catch (Exception ex) { diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index f28715dc1..b72733983 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -476,6 +476,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s "launcher.log", // XIVLauncher.Core for [mostly] Linux "patcher.log", "dalamud.log", + "dalamud.troubleshooting.json", "dalamud.injector.log", "dalamud.boot.log", "aria.log", @@ -693,7 +694,7 @@ void restart_game_using_injector(int nRadioButton, const std::vector cpui; int nIds_; From b9c4c97eba8c6c83d6511452e294ca1f862b700d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 25 Jan 2026 16:23:24 +0100 Subject: [PATCH 427/477] Add timestamp to TroubleshootingPayload --- Dalamud/Support/Troubleshooting.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index e94252027..f9e084db8 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -69,6 +69,7 @@ public static class Troubleshooting { var payload = new TroubleshootingPayload { + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(), PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()), EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(), @@ -111,6 +112,8 @@ public static class Troubleshooting private class TroubleshootingPayload { + public long Timestamp { get; set; } + public LocalPluginManifest[]? LoadedPlugins { get; set; } public Dictionary? PluginStates { get; set; } From 672636c3bf2fa995c95aff2ab70ad4af3475f605 Mon Sep 17 00:00:00 2001 From: Infi Date: Mon, 26 Jan 2026 04:21:03 +0100 Subject: [PATCH 428/477] Remove UiDebug V1 in favor of V2 (#2586) * - Remove UiDebug1 in favor of UiDebug2 * - Remove all mentions of 2 --- Dalamud/Interface/Internal/UiDebug.cs | 675 ------------------ .../Browsing/AddonTree.AtkValues.cs | 4 +- .../Browsing/AddonTree.FieldNames.cs | 4 +- .../Browsing/AddonTree.cs | 8 +- .../{UiDebug2 => UiDebug}/Browsing/Events.cs | 2 +- .../Browsing/NodeTree.ClippingMask.cs | 2 +- .../Browsing/NodeTree.Collision.cs | 2 +- .../Browsing/NodeTree.Component.cs | 4 +- .../Browsing/NodeTree.Counter.cs | 4 +- .../Browsing/NodeTree.Editor.cs | 6 +- .../Browsing/NodeTree.Image.cs | 4 +- .../Browsing/NodeTree.NineGrid.cs | 4 +- .../Browsing/NodeTree.Res.cs | 12 +- .../Browsing/NodeTree.Text.cs | 4 +- .../Browsing/TimelineTree.KeyGroupColumn.cs | 2 +- .../Browsing/TimelineTree.cs | 4 +- .../{UiDebug2 => UiDebug}/ElementSelector.cs | 20 +- .../{UiDebug2 => UiDebug}/Popout.Addon.cs | 4 +- .../{UiDebug2 => UiDebug}/Popout.Node.cs | 6 +- .../UiDebug.Sidebar.cs} | 6 +- .../UiDebug2.cs => UiDebug/UiDebug.cs} | 12 +- .../{UiDebug2 => UiDebug}/Utility/Gui.cs | 4 +- .../Utility/NodeBounds.cs | 2 +- .../Internal/Windows/Data/DataWindow.cs | 1 - .../Data/Widgets/AddonInspectorWidget.cs | 6 +- .../Data/Widgets/AddonInspectorWidget2.cs | 35 - Dalamud/Plugin/Services/ITextureProvider.cs | 2 +- 27 files changed, 64 insertions(+), 775 deletions(-) delete mode 100644 Dalamud/Interface/Internal/UiDebug.cs rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/AddonTree.AtkValues.cs (97%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/AddonTree.FieldNames.cs (98%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/AddonTree.cs (96%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/Events.cs (97%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/NodeTree.ClippingMask.cs (95%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/NodeTree.Collision.cs (93%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/NodeTree.Component.cs (99%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/NodeTree.Counter.cs (90%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/NodeTree.Editor.cs (98%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/NodeTree.Image.cs (98%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/NodeTree.NineGrid.cs (98%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/NodeTree.Res.cs (97%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/NodeTree.Text.cs (96%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/TimelineTree.KeyGroupColumn.cs (98%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Browsing/TimelineTree.cs (99%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/ElementSelector.cs (95%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Popout.Addon.cs (93%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Popout.Node.cs (93%) rename Dalamud/Interface/Internal/{UiDebug2/UiDebug2.Sidebar.cs => UiDebug/UiDebug.Sidebar.cs} (98%) rename Dalamud/Interface/Internal/{UiDebug2/UiDebug2.cs => UiDebug/UiDebug.cs} (92%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Utility/Gui.cs (97%) rename Dalamud/Interface/Internal/{UiDebug2 => UiDebug}/Utility/NodeBounds.cs (99%) delete mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget2.cs diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs deleted file mode 100644 index a79cc1880..000000000 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ /dev/null @@ -1,675 +0,0 @@ -using System.Numerics; - -using Dalamud.Bindings.ImGui; -using Dalamud.Game; -using Dalamud.Game.Gui; -using Dalamud.Interface.ImGuiSeStringRenderer.Internal; -using Dalamud.Interface.Textures.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Utility; - -using FFXIVClientStructs.FFXIV.Client.System.String; -using FFXIVClientStructs.FFXIV.Client.UI.Misc; -using FFXIVClientStructs.FFXIV.Component.GUI; - -// Customised version of https://github.com/aers/FFXIVUIDebug - -namespace Dalamud.Interface.Internal; - -/// -/// This class displays a debug window to inspect native addons. -/// -internal unsafe class UiDebug -{ - private const int UnitListCount = 18; - - private readonly bool[] selectedInList = new bool[UnitListCount]; - private readonly string[] listNames = - [ - "Depth Layer 1", - "Depth Layer 2", - "Depth Layer 3", - "Depth Layer 4", - "Depth Layer 5", - "Depth Layer 6", - "Depth Layer 7", - "Depth Layer 8", - "Depth Layer 9", - "Depth Layer 10", - "Depth Layer 11", - "Depth Layer 12", - "Depth Layer 13", - "Loaded Units", - "Focused Units", - "Units 16", - "Units 17", - "Units 18", - ]; - - private bool doingSearch; - private string searchInput = string.Empty; - private AtkUnitBase* selectedUnitBase = null; - - /// - /// Initializes a new instance of the class. - /// - public UiDebug() - { - } - - /// - /// Renders this window. - /// - public void Draw() - { - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(3, 2)); - ImGui.BeginChild("st_uiDebug_unitBaseSelect"u8, new Vector2(250, -1), true); - - ImGui.SetNextItemWidth(-1); - ImGui.InputTextWithHint("###atkUnitBaseSearch"u8, "Search"u8, ref this.searchInput, 0x20); - - this.DrawUnitBaseList(); - ImGui.EndChild(); - if (this.selectedUnitBase != null) - { - ImGui.SameLine(); - ImGui.BeginChild("st_uiDebug_selectedUnitBase"u8, new Vector2(-1, -1), true); - this.DrawUnitBase(this.selectedUnitBase); - ImGui.EndChild(); - } - - ImGui.PopStyleVar(); - } - - private void DrawUnitBase(AtkUnitBase* atkUnitBase) - { - var isVisible = atkUnitBase->IsVisible; - var addonName = atkUnitBase->NameString; - var agent = Service.Get().FindAgentInterface(atkUnitBase); - - ImGui.Text(addonName); - ImGui.SameLine(); - ImGui.PushStyleColor(ImGuiCol.Text, isVisible ? 0xFF00FF00 : 0xFF0000FF); - ImGui.Text(isVisible ? "Visible" : "Not Visible"); - ImGui.PopStyleColor(); - - ImGui.SameLine(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - 25); - if (ImGui.SmallButton("V"u8)) - { - atkUnitBase->IsVisible = !atkUnitBase->IsVisible; - } - - ImGui.Separator(); - ImGuiHelpers.ClickToCopyText($"Address: {(nint)atkUnitBase:X}", $"{(nint)atkUnitBase:X}"); - ImGuiHelpers.ClickToCopyText($"Agent: {(nint)agent:X}", $"{(nint)agent:X}"); - ImGui.Separator(); - - ImGui.Text($"Position: [ {atkUnitBase->X} , {atkUnitBase->Y} ]"); - ImGui.Text($"Scale: {atkUnitBase->Scale * 100}%"); - ImGui.Text($"Widget Count {atkUnitBase->UldManager.ObjectCount}"); - - ImGui.Separator(); - - object addonObj = *atkUnitBase; - - Util.ShowStruct(addonObj, (ulong)atkUnitBase); - - ImGui.Dummy(new Vector2(25 * ImGui.GetIO().FontGlobalScale)); - ImGui.Separator(); - if (atkUnitBase->RootNode != null) - this.PrintNode(atkUnitBase->RootNode); - - if (atkUnitBase->UldManager.NodeListCount > 0) - { - ImGui.Dummy(new Vector2(25 * ImGui.GetIO().FontGlobalScale)); - ImGui.Separator(); - ImGui.PushStyleColor(ImGuiCol.Text, 0xFFFFAAAA); - if (ImGui.TreeNode($"Node List##{(ulong)atkUnitBase:X}")) - { - ImGui.PopStyleColor(); - - for (var j = 0; j < atkUnitBase->UldManager.NodeListCount; j++) - { - this.PrintNode(atkUnitBase->UldManager.NodeList[j], false, $"[{j}] "); - } - - ImGui.TreePop(); - } - else - { - ImGui.PopStyleColor(); - } - } - } - - private void PrintNode(AtkResNode* node, bool printSiblings = true, string treePrefix = "") - { - if (node == null) - return; - - if ((int)node->Type < 1000) - this.PrintSimpleNode(node, treePrefix); - else - this.PrintComponentNode(node, treePrefix); - - if (printSiblings) - { - var prevNode = node; - while ((prevNode = prevNode->PrevSiblingNode) != null) - this.PrintNode(prevNode, false, "prev "); - - var nextNode = node; - while ((nextNode = nextNode->NextSiblingNode) != null) - this.PrintNode(nextNode, false, "next "); - } - } - - private void PrintSimpleNode(AtkResNode* node, string treePrefix) - { - var popped = false; - var isVisible = node->NodeFlags.HasFlag(NodeFlags.Visible); - - if (isVisible) - ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 255, 0, 255)); - - if (ImGui.TreeNode($"{treePrefix}{node->Type} Node (ptr = {(long)node:X})###{(long)node}")) - { - if (ImGui.IsItemHovered()) - this.DrawOutline(node); - - if (isVisible) - { - ImGui.PopStyleColor(); - popped = true; - } - - ImGui.Text("Node: "u8); - ImGui.SameLine(); - ImGuiHelpers.ClickToCopyText($"{(ulong)node:X}"); - ImGui.SameLine(); - switch (node->Type) - { - case NodeType.Text: Util.ShowStruct(*(AtkTextNode*)node, (ulong)node); break; - case NodeType.Image: Util.ShowStruct(*(AtkImageNode*)node, (ulong)node); break; - case NodeType.Collision: Util.ShowStruct(*(AtkCollisionNode*)node, (ulong)node); break; - case NodeType.NineGrid: Util.ShowStruct(*(AtkNineGridNode*)node, (ulong)node); break; - case NodeType.ClippingMask: Util.ShowStruct(*(AtkClippingMaskNode*)node, (ulong)node); break; - case NodeType.Counter: Util.ShowStruct(*(AtkCounterNode*)node, (ulong)node); break; - default: Util.ShowStruct(*node, (ulong)node); break; - } - - this.PrintResNode(node); - - if (node->ChildNode != null) - this.PrintNode(node->ChildNode); - - switch (node->Type) - { - case NodeType.Text: - var textNode = (AtkTextNode*)node; - ImGui.Text("text: "u8); - ImGui.SameLine(); - Service.Get().Draw(textNode->NodeText); - - ImGui.InputText($"Replace Text##{(ulong)textNode:X}", new(textNode->NodeText.StringPtr, (int)textNode->NodeText.BufSize)); - - ImGui.SameLine(); - if (ImGui.Button($"Encode##{(ulong)textNode:X}")) - { - using var tmp = new Utf8String(); - RaptureTextModule.Instance()->MacroEncoder.EncodeString(&tmp, textNode->NodeText.StringPtr); - textNode->NodeText.Copy(&tmp); - } - - ImGui.SameLine(); - if (ImGui.Button($"Decode##{(ulong)textNode:X}")) - textNode->NodeText.SetString(textNode->NodeText.StringPtr.AsReadOnlySeStringSpan().ToString()); - - ImGui.Text($"AlignmentType: {(AlignmentType)textNode->AlignmentFontType} FontSize: {textNode->FontSize}"); - int b = textNode->AlignmentFontType; - if (ImGui.InputInt($"###setAlignment{(ulong)textNode:X}", ref b, 1)) - { - while (b > byte.MaxValue) b -= byte.MaxValue; - while (b < byte.MinValue) b += byte.MaxValue; - textNode->AlignmentFontType = (byte)b; - textNode->AtkResNode.DrawFlags |= 0x1; - } - - ImGui.Text($"Color: #{textNode->TextColor.R:X2}{textNode->TextColor.G:X2}{textNode->TextColor.B:X2}{textNode->TextColor.A:X2}"); - ImGui.SameLine(); - ImGui.Text($"EdgeColor: #{textNode->EdgeColor.R:X2}{textNode->EdgeColor.G:X2}{textNode->EdgeColor.B:X2}{textNode->EdgeColor.A:X2}"); - ImGui.SameLine(); - ImGui.Text($"BGColor: #{textNode->BackgroundColor.R:X2}{textNode->BackgroundColor.G:X2}{textNode->BackgroundColor.B:X2}{textNode->BackgroundColor.A:X2}"); - - ImGui.Text($"TextFlags: {textNode->TextFlags}"); - - break; - case NodeType.Counter: - var counterNode = (AtkCounterNode*)node; - ImGui.Text("text: "u8); - ImGui.SameLine(); - Service.Get().Draw(counterNode->NodeText); - break; - case NodeType.Image: - var imageNode = (AtkImageNode*)node; - PrintTextureInfo(imageNode->PartsList, imageNode->PartId); - break; - case NodeType.NineGrid: - var ngNode = (AtkNineGridNode*)node; - PrintTextureInfo(ngNode->PartsList, ngNode->PartId); - break; - case NodeType.ClippingMask: - var cmNode = (AtkClippingMaskNode*)node; - PrintTextureInfo(cmNode->PartsList, cmNode->PartId); - break; - } - - ImGui.TreePop(); - } - else if (ImGui.IsItemHovered()) - { - this.DrawOutline(node); - } - - if (isVisible && !popped) - ImGui.PopStyleColor(); - - static void PrintTextureInfo(AtkUldPartsList* partsList, uint partId) - { - if (partsList != null) - { - if (partId > partsList->PartCount) - { - ImGui.Text("part id > part count?"u8); - } - else - { - var textureInfo = partsList->Parts[partId].UldAsset; - var texType = textureInfo->AtkTexture.TextureType; - ImGui.Text( - $"texture type: {texType} part_id={partId} part_id_count={partsList->PartCount}"); - if (texType == TextureType.Resource) - { - ImGui.Text( - $"texture path: {textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName}"); - var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject; - - if (ImGui.TreeNode($"Texture##{(ulong)kernelTexture->D3D11ShaderResourceView:X}")) - { - ImGui.Image( - new ImTextureID(kernelTexture->D3D11ShaderResourceView), - new Vector2(kernelTexture->ActualWidth, kernelTexture->ActualHeight)); - ImGui.TreePop(); - } - } - else if (texType == TextureType.KernelTexture) - { - if (ImGui.TreeNode( - $"Texture##{(ulong)textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView:X}")) - { - ImGui.Image( - new ImTextureID(textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView), - new Vector2( - textureInfo->AtkTexture.KernelTexture->ActualWidth, - textureInfo->AtkTexture.KernelTexture->ActualHeight)); - ImGui.TreePop(); - } - } - - if (ImGui.Button($"Replace with a random image##{(ulong)textureInfo:X}")) - { - var texm = Service.Get(); - texm.Shared - .GetFromGame( - Random.Shared.Next(0, 1) == 0 - ? $"ui/loadingimage/-nowloading_base{Random.Shared.Next(1, 33)}.tex" - : $"ui/loadingimage/-nowloading_base{Random.Shared.Next(1, 33)}_hr1.tex") - .RentAsync() - .ContinueWith( - r => Service.Get().RunOnFrameworkThread( - () => - { - if (!r.IsCompletedSuccessfully) - return; - - using (r.Result) - { - textureInfo->AtkTexture.ReleaseTexture(); - textureInfo->AtkTexture.KernelTexture = - texm.ConvertToKernelTexture(r.Result); - textureInfo->AtkTexture.TextureType = TextureType.KernelTexture; - } - })); - } - } - } - else - { - ImGui.Text("no texture loaded"u8); - } - } - } - - private void PrintComponentNode(AtkResNode* node, string treePrefix) - { - var compNode = (AtkComponentNode*)node; - - var popped = false; - var isVisible = node->NodeFlags.HasFlag(NodeFlags.Visible); - - var componentInfo = compNode->Component->UldManager; - - var childCount = componentInfo.NodeListCount; - - var objectInfo = (AtkUldComponentInfo*)componentInfo.Objects; - if (objectInfo == null) - { - return; - } - - if (isVisible) - ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 255, 0, 255)); - - if (ImGui.TreeNode($"{treePrefix}{objectInfo->ComponentType} Component Node (ptr = {(long)node:X}, component ptr = {(long)compNode->Component:X}) child count = {childCount} ###{(long)node}")) - { - if (ImGui.IsItemHovered()) - this.DrawOutline(node); - - if (isVisible) - { - ImGui.PopStyleColor(); - popped = true; - } - - ImGui.Text("Node: "u8); - ImGui.SameLine(); - ImGuiHelpers.ClickToCopyText($"{(ulong)node:X}"); - ImGui.SameLine(); - Util.ShowStruct(*compNode, (ulong)compNode); - ImGui.Text("Component: "u8); - ImGui.SameLine(); - ImGuiHelpers.ClickToCopyText($"{(ulong)compNode->Component:X}"); - ImGui.SameLine(); - - switch (objectInfo->ComponentType) - { - case ComponentType.Button: Util.ShowStruct(*(AtkComponentButton*)compNode->Component, (ulong)compNode->Component); break; - case ComponentType.Slider: Util.ShowStruct(*(AtkComponentSlider*)compNode->Component, (ulong)compNode->Component); break; - case ComponentType.Window: Util.ShowStruct(*(AtkComponentWindow*)compNode->Component, (ulong)compNode->Component); break; - case ComponentType.CheckBox: Util.ShowStruct(*(AtkComponentCheckBox*)compNode->Component, (ulong)compNode->Component); break; - case ComponentType.GaugeBar: Util.ShowStruct(*(AtkComponentGaugeBar*)compNode->Component, (ulong)compNode->Component); break; - case ComponentType.RadioButton: Util.ShowStruct(*(AtkComponentRadioButton*)compNode->Component, (ulong)compNode->Component); break; - case ComponentType.TextInput: Util.ShowStruct(*(AtkComponentTextInput*)compNode->Component, (ulong)compNode->Component); break; - case ComponentType.Icon: Util.ShowStruct(*(AtkComponentIcon*)compNode->Component, (ulong)compNode->Component); break; - default: Util.ShowStruct(*compNode->Component, (ulong)compNode->Component); break; - } - - this.PrintResNode(node); - this.PrintNode(componentInfo.RootNode); - - switch (objectInfo->ComponentType) - { - case ComponentType.TextInput: - var textInputComponent = (AtkComponentTextInput*)compNode->Component; - ImGui.Text("InputBase Text1: "u8); - ImGui.SameLine(); - Service.Get().Draw(textInputComponent->AtkComponentInputBase.EvaluatedString); - - ImGui.Text("InputBase Text2: "u8); - ImGui.SameLine(); - Service.Get().Draw(textInputComponent->AtkComponentInputBase.RawString); - - // ImGui.Text("Text1: "u8); - // ImGui.SameLine(); - // Service.Get().Draw(textInputComponent->UnkText01); - // - // ImGui.Text("Text2: "u8); - // ImGui.SameLine(); - // Service.Get().Draw(textInputComponent->UnkText02); - - ImGui.Text("AvailableLines: "u8); - ImGui.SameLine(); - Service.Get().Draw(textInputComponent->AvailableLines); - - ImGui.Text("HighlightedAutoTranslateOptionColorPrefix: "u8); - ImGui.SameLine(); - Service.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorPrefix); - - ImGui.Text("HighlightedAutoTranslateOptionColorSuffix: "u8); - ImGui.SameLine(); - Service.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorSuffix); - break; - } - - ImGui.PushStyleColor(ImGuiCol.Text, 0xFFFFAAAA); - if (ImGui.TreeNode($"Node List##{(ulong)node:X}")) - { - ImGui.PopStyleColor(); - - for (var i = 0; i < compNode->Component->UldManager.NodeListCount; i++) - { - this.PrintNode(compNode->Component->UldManager.NodeList[i], false, $"[{i}] "); - } - - ImGui.TreePop(); - } - else - { - ImGui.PopStyleColor(); - } - - ImGui.TreePop(); - } - else if (ImGui.IsItemHovered()) - { - this.DrawOutline(node); - } - - if (isVisible && !popped) - ImGui.PopStyleColor(); - } - - private void PrintResNode(AtkResNode* node) - { - ImGui.Text($"NodeID: {node->NodeId}"); - ImGui.SameLine(); - if (ImGui.SmallButton($"T:Visible##{(ulong)node:X}")) - { - node->NodeFlags ^= NodeFlags.Visible; - } - - ImGui.SameLine(); - if (ImGui.SmallButton($"C:Ptr##{(ulong)node:X}")) - { - ImGui.SetClipboardText($"{(ulong)node:X}"); - } - - ImGui.Text( - $"X: {node->X} Y: {node->Y} " + - $"ScaleX: {node->ScaleX} ScaleY: {node->ScaleY} " + - $"Rotation: {node->Rotation} " + - $"Width: {node->Width} Height: {node->Height} " + - $"OriginX: {node->OriginX} OriginY: {node->OriginY}"); - ImGui.Text( - $"RGBA: 0x{node->Color.R:X2}{node->Color.G:X2}{node->Color.B:X2}{node->Color.A:X2} " + - $"AddRGB: {node->AddRed} {node->AddGreen} {node->AddBlue} " + - $"MultiplyRGB: {node->MultiplyRed} {node->MultiplyGreen} {node->MultiplyBlue}"); - } - - private bool DrawUnitListHeader(int index, ushort count, ulong ptr, bool highlight) - { - ImGui.PushStyleColor(ImGuiCol.Text, highlight ? 0xFFAAAA00 : 0xFFFFFFFF); - if (!string.IsNullOrEmpty(this.searchInput) && !this.doingSearch) - { - ImGui.SetNextItemOpen(true, ImGuiCond.Always); - } - else if (this.doingSearch && string.IsNullOrEmpty(this.searchInput)) - { - ImGui.SetNextItemOpen(false, ImGuiCond.Always); - } - - var treeNode = ImGui.TreeNode($"{this.listNames[index]}##unitList_{index}"); - ImGui.PopStyleColor(); - - ImGui.SameLine(); - ImGui.TextDisabled($"C:{count} {ptr:X}"); - return treeNode; - } - - private void DrawUnitBaseList() - { - var foundSelected = false; - var noResults = true; - var stage = AtkStage.Instance(); - - var unitManagers = &stage->RaptureAtkUnitManager->AtkUnitManager.DepthLayerOneList; - - var searchStr = this.searchInput; - var searching = !string.IsNullOrEmpty(searchStr); - - for (var i = 0; i < UnitListCount; i++) - { - var headerDrawn = false; - - var highlight = this.selectedUnitBase != null && this.selectedInList[i]; - this.selectedInList[i] = false; - var unitManager = &unitManagers[i]; - - var headerOpen = true; - - if (!searching) - { - headerOpen = this.DrawUnitListHeader(i, unitManager->Count, (ulong)unitManager, highlight); - headerDrawn = true; - noResults = false; - } - - for (var j = 0; j < unitManager->Count && headerOpen; j++) - { - AtkUnitBase* unitBase = unitManager->Entries[j]; - if (this.selectedUnitBase != null && unitBase == this.selectedUnitBase) - { - this.selectedInList[i] = true; - foundSelected = true; - } - - var name = unitBase->NameString; - if (searching) - { - if (name == null || !name.Contains(searchStr, StringComparison.InvariantCultureIgnoreCase)) continue; - } - - noResults = false; - if (!headerDrawn) - { - headerOpen = this.DrawUnitListHeader(i, unitManager->Count, (ulong)unitManager, highlight); - headerDrawn = true; - } - - if (headerOpen) - { - var visible = unitBase->IsVisible; - ImGui.PushStyleColor(ImGuiCol.Text, visible ? 0xFF00FF00 : 0xFF999999); - - if (ImGui.Selectable($"{name}##list{i}-{(ulong)unitBase:X}_{j}", this.selectedUnitBase == unitBase)) - { - this.selectedUnitBase = unitBase; - foundSelected = true; - this.selectedInList[i] = true; - } - - ImGui.PopStyleColor(); - } - } - - if (headerDrawn && headerOpen) - { - ImGui.TreePop(); - } - - if (this.selectedInList[i] == false && this.selectedUnitBase != null) - { - for (var j = 0; j < unitManager->Count; j++) - { - AtkUnitBase* unitBase = unitManager->Entries[j]; - if (this.selectedUnitBase == null || unitBase != this.selectedUnitBase) continue; - this.selectedInList[i] = true; - foundSelected = true; - } - } - } - - if (noResults) - { - ImGui.TextDisabled("No Results"u8); - } - - if (!foundSelected) - { - this.selectedUnitBase = null; - } - - if (this.doingSearch && string.IsNullOrEmpty(this.searchInput)) - { - this.doingSearch = false; - } - else if (!this.doingSearch && !string.IsNullOrEmpty(this.searchInput)) - { - this.doingSearch = true; - } - } - - private Vector2 GetNodePosition(AtkResNode* node) - { - var pos = new Vector2(node->X, node->Y); - pos -= new Vector2(node->OriginX * (node->ScaleX - 1), node->OriginY * (node->ScaleY - 1)); - var par = node->ParentNode; - while (par != null) - { - pos *= new Vector2(par->ScaleX, par->ScaleY); - pos += new Vector2(par->X, par->Y); - pos -= new Vector2(par->OriginX * (par->ScaleX - 1), par->OriginY * (par->ScaleY - 1)); - par = par->ParentNode; - } - - return pos; - } - - private Vector2 GetNodeScale(AtkResNode* node) - { - if (node == null) return new Vector2(1, 1); - var scale = new Vector2(node->ScaleX, node->ScaleY); - while (node->ParentNode != null) - { - node = node->ParentNode; - scale *= new Vector2(node->ScaleX, node->ScaleY); - } - - return scale; - } - - private bool GetNodeVisible(AtkResNode* node) - { - if (node == null) return false; - while (node != null) - { - if (!node->NodeFlags.HasFlag(NodeFlags.Visible)) return false; - node = node->ParentNode; - } - - return true; - } - - private void DrawOutline(AtkResNode* node) - { - var position = this.GetNodePosition(node); - var scale = this.GetNodeScale(node); - var size = new Vector2(node->Width, node->Height) * scale; - - var nodeVisible = this.GetNodeVisible(node); - - position += ImGuiHelpers.MainViewport.Pos; - - ImGui.GetForegroundDrawList(ImGuiHelpers.MainViewport).AddRect(position, position + size, nodeVisible ? 0xFF00FF00 : 0xFF0000FF); - } -} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/AddonTree.AtkValues.cs similarity index 97% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/AddonTree.AtkValues.cs index ed9ed2150..646d4e3ad 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/AddonTree.AtkValues.cs @@ -1,7 +1,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Internal.UiDebug2.Utility; +using Dalamud.Interface.Internal.UiDebug.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; @@ -9,7 +9,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// public unsafe partial class AddonTree diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/AddonTree.FieldNames.cs similarity index 98% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/AddonTree.FieldNames.cs index 6ff58d657..3470b724b 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/AddonTree.FieldNames.cs @@ -7,9 +7,9 @@ using FFXIVClientStructs.Attributes; using FFXIVClientStructs.FFXIV.Component.GUI; using static System.Reflection.BindingFlags; -using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; +using static Dalamud.Interface.Internal.UiDebug.UiDebug; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// public unsafe partial class AddonTree diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/AddonTree.cs similarity index 96% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/AddonTree.cs index 2e0874206..954755708 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/AddonTree.cs @@ -8,12 +8,12 @@ using Dalamud.Interface.Components; using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Interface.FontAwesomeIcon; -using static Dalamud.Interface.Internal.UiDebug2.ElementSelector; -using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; -using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Interface.Internal.UiDebug.ElementSelector; +using static Dalamud.Interface.Internal.UiDebug.UiDebug; +using static Dalamud.Interface.Internal.UiDebug.Utility.Gui; using static Dalamud.Utility.Util; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A class representing an , allowing it to be browsed within an ImGui window. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/Events.cs similarity index 97% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/Events.cs index ed1926ce9..6e56c75f8 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/Events.cs @@ -9,7 +9,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Bindings.ImGui.ImGuiTableColumnFlags; using static Dalamud.Bindings.ImGui.ImGuiTableFlags; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// Class that prints the events table for a node, where applicable. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.ClippingMask.cs similarity index 95% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.ClippingMask.cs index cfba1a2bc..f9fb3b73d 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.ClippingMask.cs @@ -2,7 +2,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Utility.Util; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A tree for an that can be printed and browsed via ImGui. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Collision.cs similarity index 93% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Collision.cs index c447afac9..d6370d33f 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Collision.cs @@ -2,7 +2,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Utility.Util; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A tree for an that can be printed and browsed via ImGui. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Component.cs similarity index 99% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Component.cs index 13d559c11..fb4444be1 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Component.cs @@ -6,11 +6,11 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.ReadOnly; -using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Interface.Internal.UiDebug.Utility.Gui; using static Dalamud.Utility.Util; using static FFXIVClientStructs.FFXIV.Component.GUI.ComponentType; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A tree for an that can be printed and browsed via ImGui. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Counter.cs similarity index 90% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Counter.cs index 2b2adbcee..2ffcad5de 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Counter.cs @@ -2,10 +2,10 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.ReadOnly; -using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Interface.Internal.UiDebug.Utility.Gui; using static Dalamud.Utility.Util; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A tree for an that can be printed and browsed via ImGui. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Editor.cs similarity index 98% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Editor.cs index d4e8e61ab..d9dd1378c 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Editor.cs @@ -3,7 +3,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; -using Dalamud.Interface.Internal.UiDebug2.Utility; +using Dalamud.Interface.Internal.UiDebug.Utility; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -16,10 +16,10 @@ using static Dalamud.Bindings.ImGui.ImGuiTableColumnFlags; using static Dalamud.Bindings.ImGui.ImGuiTableFlags; using static Dalamud.Interface.ColorHelpers; using static Dalamud.Interface.FontAwesomeIcon; -using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Interface.Internal.UiDebug.Utility.Gui; using static Dalamud.Interface.Utility.ImGuiHelpers; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// internal unsafe partial class ResNodeTree diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Image.cs similarity index 98% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Image.cs index 45dd63b53..949197f50 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Image.cs @@ -11,11 +11,11 @@ using static Dalamud.Bindings.ImGui.ImGuiTableColumnFlags; using static Dalamud.Bindings.ImGui.ImGuiTableFlags; using static Dalamud.Bindings.ImGui.ImGuiTreeNodeFlags; using static Dalamud.Interface.ColorHelpers; -using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Interface.Internal.UiDebug.Utility.Gui; using static Dalamud.Utility.Util; using static FFXIVClientStructs.FFXIV.Component.GUI.TextureType; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A tree for an that can be printed and browsed via ImGui. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.NineGrid.cs similarity index 98% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.NineGrid.cs index 1c06dfb40..7d241ebdb 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.NineGrid.cs @@ -1,5 +1,5 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Internal.UiDebug2.Utility; +using Dalamud.Interface.Internal.UiDebug.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -9,7 +9,7 @@ using static Dalamud.Utility.Util; using Vector2 = System.Numerics.Vector2; using Vector4 = System.Numerics.Vector4; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A tree for an that can be printed and browsed via ImGui. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Res.cs similarity index 97% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Res.cs index 086a41efc..3ff64fd24 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Res.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; -using Dalamud.Interface.Internal.UiDebug2.Utility; +using Dalamud.Interface.Internal.UiDebug.Utility; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -13,14 +13,14 @@ using static Dalamud.Bindings.ImGui.ImGuiCol; using static Dalamud.Bindings.ImGui.ImGuiTreeNodeFlags; using static Dalamud.Interface.ColorHelpers; using static Dalamud.Interface.FontAwesomeIcon; -using static Dalamud.Interface.Internal.UiDebug2.Browsing.Events; -using static Dalamud.Interface.Internal.UiDebug2.ElementSelector; -using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; -using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Interface.Internal.UiDebug.Browsing.Events; +using static Dalamud.Interface.Internal.UiDebug.ElementSelector; +using static Dalamud.Interface.Internal.UiDebug.UiDebug; +using static Dalamud.Interface.Internal.UiDebug.Utility.Gui; using static Dalamud.Utility.Util; using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A tree for an that can be printed and browsed via ImGui. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Text.cs similarity index 96% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Text.cs index 7ae0d8fca..74e14b683 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/NodeTree.Text.cs @@ -12,10 +12,10 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.ReadOnly; using static Dalamud.Interface.ColorHelpers; -using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Interface.Internal.UiDebug.Utility.Gui; using static Dalamud.Utility.Util; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A tree for an that can be printed and browsed via ImGui. diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/TimelineTree.KeyGroupColumn.cs similarity index 98% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/TimelineTree.KeyGroupColumn.cs index 71323088b..910762d97 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/TimelineTree.KeyGroupColumn.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Dalamud.Bindings.ImGui; -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// public readonly partial struct TimelineTree diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs b/Dalamud/Interface/Internal/UiDebug/Browsing/TimelineTree.cs similarity index 99% rename from Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs rename to Dalamud/Interface/Internal/UiDebug/Browsing/TimelineTree.cs index 10d3d9362..a4d7151c0 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs +++ b/Dalamud/Interface/Internal/UiDebug/Browsing/TimelineTree.cs @@ -13,12 +13,12 @@ using static Dalamud.Bindings.ImGui.ImGuiTableColumnFlags; using static Dalamud.Bindings.ImGui.ImGuiTableFlags; using static Dalamud.Bindings.ImGui.ImGuiTreeNodeFlags; using static Dalamud.Interface.ColorHelpers; -using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Interface.Internal.UiDebug.Utility.Gui; using static Dalamud.Utility.Util; using static FFXIVClientStructs.FFXIV.Component.GUI.NodeType; // ReSharper disable SuggestBaseTypeForParameter -namespace Dalamud.Interface.Internal.UiDebug2.Browsing; +namespace Dalamud.Interface.Internal.UiDebug.Browsing; /// /// A struct allowing a node's animation timeline to be printed and browsed. diff --git a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs b/Dalamud/Interface/Internal/UiDebug/ElementSelector.cs similarity index 95% rename from Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs rename to Dalamud/Interface/Internal/UiDebug/ElementSelector.cs index 46e0c1f8f..808ff25d7 100644 --- a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs +++ b/Dalamud/Interface/Internal/UiDebug/ElementSelector.cs @@ -5,8 +5,8 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; -using Dalamud.Interface.Internal.UiDebug2.Browsing; -using Dalamud.Interface.Internal.UiDebug2.Utility; +using Dalamud.Interface.Internal.UiDebug.Browsing; +using Dalamud.Interface.Internal.UiDebug.Utility; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -16,7 +16,7 @@ using static System.Globalization.NumberFormatInfo; using static Dalamud.Bindings.ImGui.ImGuiCol; using static Dalamud.Bindings.ImGui.ImGuiWindowFlags; using static Dalamud.Interface.FontAwesomeIcon; -using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; +using static Dalamud.Interface.Internal.UiDebug.UiDebug; using static Dalamud.Interface.UiBuilder; using static Dalamud.Interface.Utility.ImGuiHelpers; using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags; @@ -25,7 +25,7 @@ using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags; #pragma warning disable CS0659 -namespace Dalamud.Interface.Internal.UiDebug2; +namespace Dalamud.Interface.Internal.UiDebug; /// /// A tool that enables the user to select UI elements within the inspector by mousing over them onscreen. @@ -34,7 +34,7 @@ internal unsafe class ElementSelector : IDisposable { private const int UnitListCount = 18; - private readonly UiDebug2 uiDebug2; + private readonly UiDebug uiDebug; private string addressSearchInput = string.Empty; @@ -43,10 +43,10 @@ internal unsafe class ElementSelector : IDisposable /// /// Initializes a new instance of the class. /// - /// The instance of this Element Selector belongs to. - internal ElementSelector(UiDebug2 uiDebug2) + /// The instance of this Element Selector belongs to. + internal ElementSelector(UiDebug uiDebug) { - this.uiDebug2 = uiDebug2; + this.uiDebug = uiDebug; } /// @@ -181,7 +181,7 @@ internal unsafe class ElementSelector : IDisposable { this.Active = false; - this.uiDebug2.SelectedAddonName = a.Addon->NameString; + this.uiDebug.SelectedAddonName = a.Addon->NameString; var ptrList = new List { (nint)n.Node }; @@ -420,7 +420,7 @@ internal unsafe class ElementSelector : IDisposable var addon = unitManager->Entries[j].Value; if ((nint)addon == address || FindByAddress(addon, address)) { - this.uiDebug2.SelectedAddonName = addon->NameString; + this.uiDebug.SelectedAddonName = addon->NameString; return; } } diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs b/Dalamud/Interface/Internal/UiDebug/Popout.Addon.cs similarity index 93% rename from Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs rename to Dalamud/Interface/Internal/UiDebug/Popout.Addon.cs index 69fbc17fb..cc80a27c4 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs +++ b/Dalamud/Interface/Internal/UiDebug/Popout.Addon.cs @@ -1,11 +1,11 @@ using System.Numerics; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Internal.UiDebug2.Browsing; +using Dalamud.Interface.Internal.UiDebug.Browsing; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -namespace Dalamud.Interface.Internal.UiDebug2; +namespace Dalamud.Interface.Internal.UiDebug; /// /// A popout window for an . diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs b/Dalamud/Interface/Internal/UiDebug/Popout.Node.cs similarity index 93% rename from Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs rename to Dalamud/Interface/Internal/UiDebug/Popout.Node.cs index de476983f..7d955f7f5 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs +++ b/Dalamud/Interface/Internal/UiDebug/Popout.Node.cs @@ -1,15 +1,15 @@ using System.Numerics; using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Internal.UiDebug2.Browsing; +using Dalamud.Interface.Internal.UiDebug.Browsing; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using FFXIVClientStructs.FFXIV.Component.GUI; -using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; +using static Dalamud.Interface.Internal.UiDebug.UiDebug; -namespace Dalamud.Interface.Internal.UiDebug2; +namespace Dalamud.Interface.Internal.UiDebug; /// /// A popout window for a . diff --git a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs b/Dalamud/Interface/Internal/UiDebug/UiDebug.Sidebar.cs similarity index 98% rename from Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs rename to Dalamud/Interface/Internal/UiDebug/UiDebug.Sidebar.cs index 14da58d94..0a24d4572 100644 --- a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs +++ b/Dalamud/Interface/Internal/UiDebug/UiDebug.Sidebar.cs @@ -12,10 +12,10 @@ using static System.StringComparison; using static Dalamud.Interface.FontAwesomeIcon; -namespace Dalamud.Interface.Internal.UiDebug2; +namespace Dalamud.Interface.Internal.UiDebug; -/// -internal unsafe partial class UiDebug2 +/// +internal unsafe partial class UiDebug { /// /// All unit lists to check for addons. diff --git a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs b/Dalamud/Interface/Internal/UiDebug/UiDebug.cs similarity index 92% rename from Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs rename to Dalamud/Interface/Internal/UiDebug/UiDebug.cs index 2aaef9256..bd7426466 100644 --- a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs +++ b/Dalamud/Interface/Internal/UiDebug/UiDebug.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui; -using Dalamud.Interface.Internal.UiDebug2.Browsing; +using Dalamud.Interface.Internal.UiDebug.Browsing; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; @@ -12,7 +12,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Bindings.ImGui.ImGuiWindowFlags; -namespace Dalamud.Interface.Internal.UiDebug2; +namespace Dalamud.Interface.Internal.UiDebug; // Original version by aers https://github.com/aers/FFXIVUIDebug // Also incorporates features from Caraxi's fork https://github.com/Caraxi/SimpleTweaksPlugin/blob/main/Debugging/UIDebug.cs @@ -20,17 +20,17 @@ namespace Dalamud.Interface.Internal.UiDebug2; /// /// A tool for browsing the contents and structure of UI elements. /// -internal partial class UiDebug2 : IDisposable +internal partial class UiDebug : IDisposable { /// - internal static readonly ModuleLog Log = ModuleLog.Create(); + internal static readonly ModuleLog Log = ModuleLog.Create(); private readonly ElementSelector elementSelector; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - internal UiDebug2() + internal UiDebug() { this.elementSelector = new(this); } diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs b/Dalamud/Interface/Internal/UiDebug/Utility/Gui.cs similarity index 97% rename from Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs rename to Dalamud/Interface/Internal/UiDebug/Utility/Gui.cs index adfbfa81c..2c02fd793 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs +++ b/Dalamud/Interface/Internal/UiDebug/Utility/Gui.cs @@ -9,10 +9,10 @@ using FFXIVClientStructs.FFXIV.Client.Graphics; using static Dalamud.Bindings.ImGui.ImGuiCol; using static Dalamud.Interface.ColorHelpers; -namespace Dalamud.Interface.Internal.UiDebug2.Utility; +namespace Dalamud.Interface.Internal.UiDebug.Utility; /// -/// Miscellaneous ImGui tools used by . +/// Miscellaneous ImGui tools used by . /// internal static class Gui { diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs b/Dalamud/Interface/Internal/UiDebug/Utility/NodeBounds.cs similarity index 99% rename from Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs rename to Dalamud/Interface/Internal/UiDebug/Utility/NodeBounds.cs index 20feb903f..414d49cf5 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs +++ b/Dalamud/Interface/Internal/UiDebug/Utility/NodeBounds.cs @@ -11,7 +11,7 @@ using static System.MathF; using static Dalamud.Interface.ColorHelpers; -namespace Dalamud.Interface.Internal.UiDebug2.Utility; +namespace Dalamud.Interface.Internal.UiDebug.Utility; /// /// A struct representing the perimeter of an , accounting for all transformations. diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 444b923ab..64de5e87d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -21,7 +21,6 @@ internal class DataWindow : Window, IDisposable private readonly IDataWindowWidget[] modules = [ new AddonInspectorWidget(), - new AddonInspectorWidget2(), new AddonLifecycleWidget(), new AddonWidget(), new AddressesWidget(), diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs index c8a747239..95a5616b1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs @@ -5,8 +5,8 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class AddonInspectorWidget : IDataWindowWidget { - private UiDebug? addonInspector; - + private UiDebug.UiDebug? addonInspector; + /// public string[]? CommandShortcuts { get; init; } = ["ai", "addoninspector"]; @@ -19,7 +19,7 @@ internal class AddonInspectorWidget : IDataWindowWidget /// public void Load() { - this.addonInspector = new UiDebug(); + this.addonInspector = new UiDebug.UiDebug(); if (this.addonInspector is not null) { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget2.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget2.cs deleted file mode 100644 index 6cd6ecb0b..000000000 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget2.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Dalamud.Interface.Internal.Windows.Data.Widgets; - -/// -/// Widget for displaying addon inspector. -/// -internal class AddonInspectorWidget2 : IDataWindowWidget -{ - private UiDebug2.UiDebug2? addonInspector2; - - /// - public string[]? CommandShortcuts { get; init; } = ["ai2", "addoninspector2"]; - - /// - public string DisplayName { get; init; } = "Addon Inspector v2 (Testing)"; - - /// - public bool Ready { get; set; } - - /// - public void Load() - { - this.addonInspector2 = new UiDebug2.UiDebug2(); - - if (this.addonInspector2 is not null) - { - this.Ready = true; - } - } - - /// - public void Draw() - { - this.addonInspector2?.Draw(); - } -} diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index dc0522aa8..a4d1dcbd2 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -322,7 +322,7 @@ public interface ITextureProvider : IDalamudService /// Whether to leave non-disposed when the returned /// completes. /// Address of the new . - /// See PrintTextureInfo in for an example + /// See PrintTextureInfo in for an example /// of replacing the texture of an image node. /// /// If the returned kernel texture is to be destroyed, call the fourth function in its vtable, by calling From 8f8f4faa12dc0ea028546cc60665159ac62b737b Mon Sep 17 00:00:00 2001 From: Infi Date: Mon, 26 Jan 2026 04:21:33 +0100 Subject: [PATCH 429/477] Apply ImRaii to Widgets Part 2 (#2567) * Apply ImRaii to multiple widgets * Apply ImRaii to leftover widgets --- .../Windows/Data/Widgets/AddonWidget.cs | 2 +- .../Windows/Data/Widgets/AddressesWidget.cs | 17 ++- .../Windows/Data/Widgets/AetherytesWidget.cs | 8 +- .../Data/Widgets/AtkArrayDataBrowserWidget.cs | 27 ++-- .../Windows/Data/Widgets/BuddyListWidget.cs | 111 ++++++++-------- .../Windows/Data/Widgets/CommandWidget.cs | 8 +- .../Data/Widgets/ConfigurationWidget.cs | 4 +- .../Windows/Data/Widgets/DataShareWidget.cs | 124 ++++++++---------- .../Windows/Data/Widgets/FateTableWidget.cs | 13 +- .../Windows/Data/Widgets/FlyTextWidget.cs | 15 ++- .../Widgets/GamePrebakedFontsTestWidget.cs | 6 +- .../Windows/Data/Widgets/GamepadWidget.cs | 20 +-- .../Windows/Data/Widgets/GaugeWidget.cs | 5 +- .../Windows/Data/Widgets/HookWidget.cs | 114 ++++++++-------- .../Windows/Data/Widgets/IconBrowserWidget.cs | 63 ++++----- .../Windows/Data/Widgets/ImGuiWidget.cs | 45 ++----- .../Windows/Data/Widgets/InventoryWidget.cs | 46 +++---- .../Windows/Data/Widgets/KeyStateWidget.cs | 11 +- .../Windows/Data/Widgets/MarketBoardWidget.cs | 74 +++++------ .../Data/Widgets/NetworkMonitorWidget.cs | 2 +- .../Data/Widgets/NounProcessorWidget.cs | 5 +- .../Windows/Data/Widgets/ObjectTableWidget.cs | 105 +++++++-------- .../Windows/Data/Widgets/PartyListWidget.cs | 8 +- .../Data/Widgets/SeStringCreatorWidget.cs | 42 +++--- .../Windows/Data/Widgets/ServicesWidget.cs | 20 +-- 25 files changed, 411 insertions(+), 484 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs index c0f923fc7..85ccd471d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs @@ -8,7 +8,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying Addon Data. /// -internal unsafe class AddonWidget : IDataWindowWidget +internal class AddonWidget : IDataWindowWidget { private string inputAddonName = string.Empty; private int inputAddonIndex; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs index 06c7ea393..6a4f2c9ab 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Dalamud.Bindings.ImGui; using Dalamud.Game; +using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -46,22 +47,24 @@ internal class AddressesWidget : IDataWindowWidget } } - ImGui.Text($"Result: {this.sigResult.ToInt64():X}"); + ImGui.AlignTextToFramePadding(); + ImGui.Text($"Result: {this.sigResult:X}"); ImGui.SameLine(); - if (ImGui.Button($"C##{this.sigResult.ToInt64():X}")) - ImGui.SetClipboardText(this.sigResult.ToInt64().ToString("X")); + if (ImGui.Button($"C##{this.sigResult:X}")) + ImGui.SetClipboardText($"{this.sigResult:X}"); foreach (var debugScannedValue in BaseAddressResolver.DebugScannedValues) { ImGui.Text($"{debugScannedValue.Key}"); foreach (var valueTuple in debugScannedValue.Value) { - ImGui.Text( - $" {valueTuple.ClassName} - {Util.DescribeAddress(valueTuple.Address)}"); + using var indent = ImRaii.PushIndent(10.0f); + ImGui.AlignTextToFramePadding(); + ImGui.Text($"{valueTuple.ClassName} - {Util.DescribeAddress(valueTuple.Address)}"); ImGui.SameLine(); - if (ImGui.Button($"C##{valueTuple.Address.ToInt64():X}")) - ImGui.SetClipboardText(valueTuple.Address.ToInt64().ToString("X")); + if (ImGui.Button($"C##{valueTuple.Address:X}")) + ImGui.SetClipboardText($"{valueTuple.Address:X}"); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs index f414a9423..68ba93b36 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs @@ -1,5 +1,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Aetherytes; +using Dalamud.Interface.Utility.Raii; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -8,6 +9,8 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class AetherytesWidget : IDataWindowWidget { + private const ImGuiTableFlags TableFlags = ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders; + /// public bool Ready { get; set; } @@ -26,7 +29,8 @@ internal class AetherytesWidget : IDataWindowWidget /// public void Draw() { - if (!ImGui.BeginTable("##aetheryteTable"u8, 11, ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders)) + using var table = ImRaii.Table("##aetheryteTable"u8, 11, TableFlags); + if (!table.Success) return; ImGui.TableSetupScrollFreeze(0, 1); @@ -84,7 +88,5 @@ internal class AetherytesWidget : IDataWindowWidget ImGui.TableNextColumn(); // Apartment ImGui.Text($"{info.IsApartment}"); } - - ImGui.EndTable(); } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs index 03f5ab32e..6f9bbdb1b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs @@ -25,10 +25,10 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget private int selectedExtendArray; private string searchTerm = string.Empty; - private bool hideUnsetStringArrayEntries = false; - private bool hideUnsetExtendArrayEntries = false; - private bool showTextAddress = false; - private bool showMacroString = false; + private bool hideUnsetStringArrayEntries; + private bool hideUnsetExtendArrayEntries; + private bool showTextAddress; + private bool showMacroString; /// public bool Ready { get; set; } @@ -155,17 +155,14 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget if (ImGui.IsItemHovered()) { using var tooltip = ImRaii.Tooltip(); - if (tooltip) + + var raptureAtkUnitManager = RaptureAtkUnitManager.Instance(); + for (var j = 0; j < array->SubscribedAddonsCount; j++) { - var raptureAtkUnitManager = RaptureAtkUnitManager.Instance(); + if (array->SubscribedAddons[j] == 0) + continue; - for (var j = 0; j < array->SubscribedAddonsCount; j++) - { - if (array->SubscribedAddons[j] == 0) - continue; - - ImGui.Text(raptureAtkUnitManager->GetAddonById(array->SubscribedAddons[j])->NameString); - } + ImGui.Text(raptureAtkUnitManager->GetAddonById(array->SubscribedAddons[j])->NameString); } } } @@ -244,9 +241,9 @@ internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget var atkArrayDataHolder = RaptureAtkModule.Instance()->AtkArrayDataHolder; - using (var sidebarchild = ImRaii.Child("StringArraySidebar"u8, new Vector2(300, -1), false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoSavedSettings)) + using (var sidebarChild = ImRaii.Child("StringArraySidebar"u8, new Vector2(300, -1), false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoSavedSettings)) { - if (sidebarchild) + if (sidebarChild) { ImGui.SetNextItemWidth(-1); ImGui.InputTextWithHint("##TextSearch"u8, "Search..."u8, ref this.searchTerm, 256, ImGuiInputTextFlags.AutoSelectAll); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs index 10efdbae1..bbef5fee3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs @@ -32,18 +32,65 @@ internal class BuddyListWidget : IDataWindowWidget var buddyList = Service.Get(); ImGui.Checkbox("Resolve GameData"u8, ref this.resolveGameData); + + var companionBuddy = buddyList.CompanionBuddy; + if (companionBuddy == null) { - var member = buddyList.CompanionBuddy; - if (member == null) + ImGui.Text("[Companion] null"u8); + } + else + { + ImGui.Text($"[Companion] {companionBuddy.Address:X} - {companionBuddy.EntityId} - {companionBuddy.DataID}"); + if (this.resolveGameData) { - ImGui.Text("[Companion] null"u8); + var gameObject = companionBuddy.GameObject; + if (gameObject == null) + { + ImGui.Text("GameObject was null"u8); + } + else + { + Util.PrintGameObject(gameObject, "-", this.resolveGameData); + } } - else + } + + var petBuddy = buddyList.PetBuddy; + if (petBuddy == null) + { + ImGui.Text("[Pet] null"u8); + } + else + { + ImGui.Text($"[Pet] {petBuddy.Address:X} - {petBuddy.EntityId} - {petBuddy.DataID}"); + if (this.resolveGameData) { - ImGui.Text($"[Companion] {member.Address.ToInt64():X} - {member.EntityId} - {member.DataID}"); + var gameObject = petBuddy.GameObject; + if (gameObject == null) + { + ImGui.Text("GameObject was null"u8); + } + else + { + Util.PrintGameObject(gameObject, "-", this.resolveGameData); + } + } + } + + var count = buddyList.Length; + if (count == 0) + { + ImGui.Text("[BattleBuddy] None present"u8); + } + else + { + for (var i = 0; i < count; i++) + { + var member = buddyList[i]; + ImGui.Text($"[BattleBuddy] [{i}] {member?.Address ?? 0:X} - {member?.EntityId ?? 0} - {member?.DataID ?? 0}"); if (this.resolveGameData) { - var gameObject = member.GameObject; + var gameObject = member?.GameObject; if (gameObject == null) { ImGui.Text("GameObject was null"u8); @@ -55,57 +102,5 @@ internal class BuddyListWidget : IDataWindowWidget } } } - - { - var member = buddyList.PetBuddy; - if (member == null) - { - ImGui.Text("[Pet] null"u8); - } - else - { - ImGui.Text($"[Pet] {member.Address.ToInt64():X} - {member.EntityId} - {member.DataID}"); - if (this.resolveGameData) - { - var gameObject = member.GameObject; - if (gameObject == null) - { - ImGui.Text("GameObject was null"u8); - } - else - { - Util.PrintGameObject(gameObject, "-", this.resolveGameData); - } - } - } - } - - { - var count = buddyList.Length; - if (count == 0) - { - ImGui.Text("[BattleBuddy] None present"u8); - } - else - { - for (var i = 0; i < count; i++) - { - var member = buddyList[i]; - ImGui.Text($"[BattleBuddy] [{i}] {member?.Address.ToInt64():X} - {member?.EntityId} - {member?.DataID}"); - if (this.resolveGameData) - { - var gameObject = member?.GameObject; - if (gameObject == null) - { - ImGui.Text("GameObject was null"u8); - } - else - { - Util.PrintGameObject(gameObject, "-", this.resolveGameData); - } - } - } - } - } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs index 1082bf6ca..e1b06aaf3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs @@ -11,6 +11,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class CommandWidget : IDataWindowWidget { + private const ImGuiTableFlags TableFlags = ImGuiTableFlags.ScrollY | ImGuiTableFlags.Borders | + ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.Sortable | + ImGuiTableFlags.SortTristate; + /// public string[]? CommandShortcuts { get; init; } = ["command"]; @@ -31,9 +35,7 @@ internal class CommandWidget : IDataWindowWidget { var commandManager = Service.Get(); - var tableFlags = ImGuiTableFlags.ScrollY | ImGuiTableFlags.Borders | ImGuiTableFlags.SizingStretchProp | - ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate; - using var table = ImRaii.Table("CommandList"u8, 4, tableFlags); + using var table = ImRaii.Table("CommandList"u8, 4, TableFlags); if (table) { ImGui.TableSetupScrollFreeze(0, 1); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs index c6a3477ae..df5aeab56 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs @@ -10,9 +10,9 @@ internal class ConfigurationWidget : IDataWindowWidget { /// public string[]? CommandShortcuts { get; init; } = ["config", "configuration"]; - + /// - public string DisplayName { get; init; } = "Configuration"; + public string DisplayName { get; init; } = "Configuration"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 8c9774840..73e9d18f8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Reflection; @@ -21,13 +20,9 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying plugin data share modules. /// -[SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] internal class DataShareWidget : IDataWindowWidget { - private const ImGuiTabItemFlags NoCloseButton = (ImGuiTabItemFlags)(1 << 20); + private const ImGuiTabItemFlags NoCloseButton = (ImGuiTabItemFlags)ImGuiTabItemFlagsPrivate.NoCloseButton; private readonly List<(string Name, byte[]? Data)> dataView = []; private int nextTab = -1; @@ -50,42 +45,37 @@ internal class DataShareWidget : IDataWindowWidget } /// - public unsafe void Draw() + public void Draw() { using var tabbar = ImRaii.TabBar("##tabbar"u8); if (!tabbar.Success) return; var d = true; - using (var tabitem = ImRaii.TabItem( - "Data Share##tabbar-datashare"u8, - ref d, - NoCloseButton | (this.nextTab == 0 ? ImGuiTabItemFlags.SetSelected : 0))) + using (var tabItem = ImRaii.TabItem("Data Share##tabbar-datashare"u8, ref d, NoCloseButton | (this.nextTab == 0 ? ImGuiTabItemFlags.SetSelected : 0))) { - if (tabitem.Success) + if (tabItem.Success) this.DrawDataShare(); } - using (var tabitem = ImRaii.TabItem( - "Call Gate##tabbar-callgate"u8, - ref d, - NoCloseButton | (this.nextTab == 1 ? ImGuiTabItemFlags.SetSelected : 0))) + using (var tabItem = ImRaii.TabItem("Call Gate##tabbar-callgate"u8, ref d, NoCloseButton | (this.nextTab == 1 ? ImGuiTabItemFlags.SetSelected : 0))) { - if (tabitem.Success) + if (tabItem.Success) this.DrawCallGate(); } for (var i = 0; i < this.dataView.Count; i++) { using var idpush = ImRaii.PushId($"##tabbar-data-{i}"); + var (name, data) = this.dataView[i]; d = true; - using var tabitem = ImRaii.TabItem( - name, - ref d, - this.nextTab == 2 + i ? ImGuiTabItemFlags.SetSelected : 0); + + using var tabitem = ImRaii.TabItem(name, ref d, this.nextTab == 2 + i ? ImGuiTabItemFlags.SetSelected : 0); + if (!d) this.dataView.RemoveAt(i--); + if (!tabitem.Success) continue; @@ -123,11 +113,7 @@ internal class DataShareWidget : IDataWindowWidget if (ImGui.Button("Copy"u8)) ImGui.SetClipboardText(data); - ImGui.InputTextMultiline( - "text"u8, - data, - ImGui.GetContentRegionAvail(), - ImGuiInputTextFlags.ReadOnly); + ImGui.InputTextMultiline("text"u8, data, ImGui.GetContentRegionAvail(), ImGuiInputTextFlags.ReadOnly); } this.nextTab = -1; @@ -142,8 +128,10 @@ internal class DataShareWidget : IDataWindowWidget sb.Append(ReprType(mi.DeclaringType)) .Append("::") .Append(mi.Name); + if (!withParams) return sb.ToString(); + sb.Append('('); var parfirst = true; foreach (var par in mi.GetParameters()) @@ -152,6 +140,7 @@ internal class DataShareWidget : IDataWindowWidget sb.Append(", "); else parfirst = false; + sb.AppendLine() .Append('\t') .Append(ReprType(par.ParameterType)) @@ -161,9 +150,11 @@ internal class DataShareWidget : IDataWindowWidget if (!parfirst) sb.AppendLine(); + sb.Append(')'); if (mi.ReturnType != typeof(void)) sb.Append(" -> ").Append(ReprType(mi.ReturnType)); + return sb.ToString(); static string WithoutGeneric(string s) @@ -172,8 +163,7 @@ internal class DataShareWidget : IDataWindowWidget return i != -1 ? s[..i] : s; } - static string ReprType(Type? t) => - t switch + static string ReprType(Type? t) => t switch { null => "null", _ when t == typeof(string) => "string", @@ -215,18 +205,19 @@ internal class DataShareWidget : IDataWindowWidget var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0); if (framepad) ImGui.AlignTextToFramePadding(); + ImGui.Text(s); if (ImGui.IsItemHovered()) { ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding); var vp = ImGui.GetWindowViewport(); var wrx = (vp.WorkPos.X + vp.WorkSize.X) - offset.X; + ImGui.SetNextWindowSizeConstraints(Vector2.One, new(wrx, float.MaxValue)); using (ImRaii.Tooltip()) { - ImGui.PushTextWrapPos(wrx); + using var pushedWrap = ImRaii.TextWrapPos(wrx); ImGui.TextWrapped(tooltip?.Invoke() ?? s); - ImGui.PopTextWrapPos(); } } @@ -247,6 +238,9 @@ internal class DataShareWidget : IDataWindowWidget callGate.PurgeEmptyGates(); using var table = ImRaii.Table("##callgate-table"u8, 5); + if (!table.Success) + return; + ImGui.TableSetupColumn("Name"u8, ImGuiTableColumnFlags.DefaultSort); ImGui.TableSetupColumn("Action"u8); ImGui.TableSetupColumn("Func"u8); @@ -268,12 +262,8 @@ internal class DataShareWidget : IDataWindowWidget { ImGui.TableNextRow(); this.DrawTextCell(item.Name); - this.DrawTextCell( - ReprMethod(item.Action?.Method, false), - () => ReprMethod(item.Action?.Method, true)); - this.DrawTextCell( - ReprMethod(item.Func?.Method, false), - () => ReprMethod(item.Func?.Method, true)); + this.DrawTextCell(ReprMethod(item.Action?.Method, false), () => ReprMethod(item.Action?.Method, true)); + this.DrawTextCell(ReprMethod(item.Func?.Method, false), () => ReprMethod(item.Func?.Method, true)); if (subs.Count == 0) { this.DrawTextCell("0"); @@ -288,47 +278,43 @@ internal class DataShareWidget : IDataWindowWidget private void DrawDataShare() { - if (!ImGui.BeginTable("###DataShareTable"u8, 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg)) + using var table = ImRaii.Table("###DataShareTable"u8, 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table.Success) return; - try + ImGui.TableSetupColumn("Shared Tag"u8); + ImGui.TableSetupColumn("Show"u8); + ImGui.TableSetupColumn("Creator Assembly"u8); + ImGui.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Consumers"u8); + ImGui.TableHeadersRow(); + + foreach (var share in Service.Get().GetAllShares()) { - ImGui.TableSetupColumn("Shared Tag"u8); - ImGui.TableSetupColumn("Show"u8); - ImGui.TableSetupColumn("Creator Assembly"u8); - ImGui.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Consumers"u8); - ImGui.TableHeadersRow(); - foreach (var share in Service.Get().GetAllShares()) + ImGui.TableNextRow(); + this.DrawTextCell(share.Tag, null, true); + + ImGui.TableNextColumn(); + if (ImGui.Button($"Show##datasharetable-show-{share.Tag}")) { - ImGui.TableNextRow(); - this.DrawTextCell(share.Tag, null, true); - - ImGui.TableNextColumn(); - if (ImGui.Button($"Show##datasharetable-show-{share.Tag}")) + var index = 0; + for (; index < this.dataView.Count; index++) { - var index = 0; - for (; index < this.dataView.Count; index++) - { - if (this.dataView[index].Name == share.Tag) - break; - } - - if (index == this.dataView.Count) - this.dataView.Add((share.Tag, null)); - else - this.dataView[index] = (share.Tag, null); - this.nextTab = 2 + index; + if (this.dataView[index].Name == share.Tag) + break; } - this.DrawTextCell(share.CreatorAssembly, null, true); - this.DrawTextCell(share.Users.Length.ToString(), null, true); - this.DrawTextCell(string.Join(", ", share.Users), null, true); + if (index == this.dataView.Count) + this.dataView.Add((share.Tag, null)); + else + this.dataView[index] = (share.Tag, null); + + this.nextTab = 2 + index; } - } - finally - { - ImGui.EndTable(); + + this.DrawTextCell(share.CreatorAssembly, null, true); + this.DrawTextCell(share.Users.Length.ToString(), null, true); + this.DrawTextCell(string.Join(", ", share.Users), null, true); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs index e241f157d..b2f4bf70f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs @@ -10,6 +10,9 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class FateTableWidget : IDataWindowWidget { + private const ImGuiTableFlags TableFlags = ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | + ImGuiTableFlags.Borders | ImGuiTableFlags.NoSavedSettings; + /// public string[]? CommandShortcuts { get; init; } = ["fate", "fatetable"]; @@ -37,7 +40,7 @@ internal class FateTableWidget : IDataWindowWidget return; } - using var table = ImRaii.Table("FateTable"u8, 13, ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.NoSavedSettings); + using var table = ImRaii.Table("FateTable"u8, 13, TableFlags); if (!table) return; ImGui.TableSetupColumn("Index"u8, ImGuiTableColumnFlags.WidthFixed, 40); @@ -97,11 +100,11 @@ internal class FateTableWidget : IDataWindowWidget if (ImGui.IsItemHovered()) { ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); - ImGui.BeginTooltip(); + + using var tooltip = ImRaii.Tooltip(); ImGui.Text("Click to copy IconId"u8); ImGui.Text($"ID: {fate.IconId} – Size: {texture.Width}x{texture.Height}"); ImGui.Image(texture.Handle, new(texture.Width, texture.Height)); - ImGui.EndTooltip(); } if (ImGui.IsItemClicked()) @@ -122,11 +125,11 @@ internal class FateTableWidget : IDataWindowWidget if (ImGui.IsItemHovered()) { ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); - ImGui.BeginTooltip(); + + using var tooltip = ImRaii.Tooltip(); ImGui.Text("Click to copy MapIconId"u8); ImGui.Text($"ID: {fate.MapIconId} – Size: {texture.Width}x{texture.Height}"); ImGui.Image(texture.Handle, new(texture.Width, texture.Height)); - ImGui.EndTooltip(); } if (ImGui.IsItemClicked()) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs index 7910daaec..00e424551 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs @@ -3,6 +3,7 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui.FlyText; +using Dalamud.Interface.Utility.Raii; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -39,18 +40,18 @@ internal class FlyTextWidget : IDataWindowWidget /// public void Draw() { - if (ImGui.BeginCombo("Kind"u8, $"{this.flyKind} ({(int)this.flyKind})")) + using (var combo = ImRaii.Combo("Kind"u8, $"{this.flyKind} ({(int)this.flyKind})")) { - var values = Enum.GetValues().Distinct(); - foreach (var value in values) + if (combo.Success) { - if (ImGui.Selectable($"{value} ({(int)value})")) + foreach (var value in Enum.GetValues().Distinct()) { - this.flyKind = value; + if (ImGui.Selectable($"{value} ({(int)value})")) + { + this.flyKind = value; + } } } - - ImGui.EndCombo(); } ImGui.InputText("Text1"u8, ref this.flyText1, 200); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index 32ad076db..0c0289bd0 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -13,6 +13,7 @@ using Dalamud.Interface.ImGuiFontChooserDialog; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using Serilog; @@ -248,7 +249,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { ImGui.Text($"{gfs.SizePt}pt"); ImGui.SameLine(offsetX); - ImGui.PushTextWrapPos(this.useWordWrap ? 0f : -1f); + + using var pushedWrap = ImRaii.TextWrapPos(this.useWordWrap ? 0f : -1f); try { if (handle.Value.LoadException is { } exc) @@ -263,6 +265,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { if (!this.atlasScaleMode) ImGui.SetWindowFontScale(1 / ImGuiHelpers.GlobalScale); + if (counter++ % 2 == 0) { using var pushPop = handle.Value.Push(); @@ -279,7 +282,6 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable finally { ImGui.SetWindowFontScale(1); - ImGui.PopTextWrapPos(); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs index ed1a4da5b..12aa4c4ae 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs @@ -39,22 +39,10 @@ internal class GamepadWidget : IDataWindowWidget ImGui.SetClipboardText($"{Util.DescribeAddress(gamepadState.GamepadInputAddress)}"); #endif - this.DrawHelper( - "Buttons Raw", - (uint)gamepadState.ButtonsRaw, - gamepadState.Raw); - this.DrawHelper( - "Buttons Pressed", - (uint)gamepadState.ButtonsPressed, - gamepadState.Pressed); - this.DrawHelper( - "Buttons Repeat", - (uint)gamepadState.ButtonsRepeat, - gamepadState.Repeat); - this.DrawHelper( - "Buttons Released", - (uint)gamepadState.ButtonsReleased, - gamepadState.Released); + this.DrawHelper("Buttons Raw", (uint)gamepadState.ButtonsRaw, gamepadState.Raw); + this.DrawHelper("Buttons Pressed", (uint)gamepadState.ButtonsPressed, gamepadState.Pressed); + this.DrawHelper("Buttons Repeat", (uint)gamepadState.ButtonsRepeat, gamepadState.Repeat); + this.DrawHelper("Buttons Released", (uint)gamepadState.ButtonsReleased, gamepadState.Released); ImGui.Text($"LeftStick {gamepadState.LeftStick}"); ImGui.Text($"RightStick {gamepadState.RightStick}"); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs index 09bd29851..74403a32b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs @@ -39,8 +39,7 @@ internal class GaugeWidget : IDataWindowWidget return; } - var jobID = player.ClassJob.RowId; - JobGaugeBase? gauge = jobID switch + JobGaugeBase? gauge = player.ClassJob.RowId switch { 19 => jobGauges.Get(), 20 => jobGauges.Get(), @@ -63,7 +62,7 @@ internal class GaugeWidget : IDataWindowWidget 40 => jobGauges.Get(), 41 => jobGauges.Get(), 42 => jobGauges.Get(), - _ => null, + _ => null }; if (gauge == null) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index ad06e12fd..fd27996ed 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Game; using Dalamud.Hooking; - +using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Component.GUI; using Serilog; @@ -26,12 +26,12 @@ internal unsafe class HookWidget : IDataWindowWidget private Hook? messageBoxMinHook; private bool hookUseMinHook; - private int hookStressTestCount = 0; + private int hookStressTestCount; private int hookStressTestMax = 1000; private int hookStressTestWait = 100; private int hookStressTestMaxDegreeOfParallelism = 10; private StressTestHookTarget hookStressTestHookTarget = StressTestHookTarget.Random; - private bool hookStressTestRunning = false; + private bool hookStressTestRunning; private MessageBoxWDelegate? messageBoxWOriginal; private AddonFinalizeDelegate? addonFinalizeOriginal; @@ -50,7 +50,7 @@ internal unsafe class HookWidget : IDataWindowWidget { MessageBoxW, AddonFinalize, - Random, + Random } /// @@ -106,67 +106,68 @@ internal unsafe class HookWidget : IDataWindowWidget ImGui.Separator(); - ImGui.BeginDisabled(this.hookStressTestRunning); - ImGui.Text("Stress Test"u8); - - if (ImGui.InputInt("Max"u8, ref this.hookStressTestMax)) - this.hookStressTestCount = 0; - - ImGui.InputInt("Wait (ms)"u8, ref this.hookStressTestWait); - ImGui.InputInt("Max Degree of Parallelism"u8, ref this.hookStressTestMaxDegreeOfParallelism); - - if (ImGui.BeginCombo("Target"u8, HookTargetToString(this.hookStressTestHookTarget))) + using (ImRaii.Disabled(this.hookStressTestRunning)) { - foreach (var target in Enum.GetValues()) + ImGui.Text("Stress Test"u8); + + if (ImGui.InputInt("Max"u8, ref this.hookStressTestMax)) + this.hookStressTestCount = 0; + + ImGui.InputInt("Wait (ms)"u8, ref this.hookStressTestWait); + ImGui.InputInt("Max Degree of Parallelism"u8, ref this.hookStressTestMaxDegreeOfParallelism); + + using (var combo = ImRaii.Combo("Target"u8, HookTargetToString(this.hookStressTestHookTarget))) { - if (ImGui.Selectable(HookTargetToString(target), this.hookStressTestHookTarget == target)) - this.hookStressTestHookTarget = target; + if (combo.Success) + { + foreach (var target in Enum.GetValues()) + { + if (ImGui.Selectable(HookTargetToString(target), this.hookStressTestHookTarget == target)) + this.hookStressTestHookTarget = target; + } + } } - ImGui.EndCombo(); - } - - if (ImGui.Button("Stress Test"u8)) - { - Task.Run(() => + if (ImGui.Button("Stress Test"u8)) { - this.hookStressTestRunning = true; - this.hookStressTestCount = 0; - Parallel.For( - 0, - this.hookStressTestMax, - new ParallelOptions + Task.Run(() => + { + this.hookStressTestRunning = true; + this.hookStressTestCount = 0; + Parallel.For( + 0, + this.hookStressTestMax, + new ParallelOptions + { + MaxDegreeOfParallelism = this.hookStressTestMaxDegreeOfParallelism, + }, + _ => + { + this.hookStressTestList.Add(this.HookTarget(this.hookStressTestHookTarget)); + this.hookStressTestCount++; + Thread.Sleep(this.hookStressTestWait); + }); + }).ContinueWith(t => + { + if (t.IsFaulted) { - MaxDegreeOfParallelism = this.hookStressTestMaxDegreeOfParallelism, - }, - _ => + Log.Error(t.Exception, "Stress test failed"); + } + else { - this.hookStressTestList.Add(this.HookTarget(this.hookStressTestHookTarget)); - this.hookStressTestCount++; - Thread.Sleep(this.hookStressTestWait); + Log.Information("Stress test completed"); + } + + this.hookStressTestRunning = false; + this.hookStressTestList.ForEach(hook => + { + hook.Dispose(); }); - }).ContinueWith(t => - { - if (t.IsFaulted) - { - Log.Error(t.Exception, "Stress test failed"); - } - else - { - Log.Information("Stress test completed"); - } - - this.hookStressTestRunning = false; - this.hookStressTestList.ForEach(hook => - { - hook.Dispose(); + this.hookStressTestList.Clear(); }); - this.hookStressTestList.Clear(); - }); + } } - ImGui.EndDisabled(); - ImGui.Text("Status: " + (this.hookStressTestRunning ? "Running" : "Idle")); ImGui.ProgressBar(this.hookStressTestCount / (float)this.hookStressTestMax, new System.Numerics.Vector2(0, 0), $"{this.hookStressTestCount}/{this.hookStressTestMax}"); } @@ -206,11 +207,6 @@ internal unsafe class HookWidget : IDataWindowWidget this.addonFinalizeOriginal!(unitManager, atkUnitBase); } - private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta) - { - Log.Information("OnAddonUpdate"); - } - private IDalamudHook HookMessageBoxW() { var hook = Hook.FromSymbol( diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index 77ca7ec2b..670b83f67 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -8,6 +8,7 @@ using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Internal; +using Dalamud.Interface.Utility.Raii; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -62,6 +63,7 @@ public class IconBrowserWidget : IDataWindowWidget // continue; if (!texm.TryGetIconPath(new((uint)iconId), out var path)) continue; + result.Add((iconId, path)); } @@ -82,17 +84,17 @@ public class IconBrowserWidget : IDataWindowWidget { this.RecalculateIndexRange(); - if (ImGui.BeginChild("ScrollableSection"u8, ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoMove)) + using (var child = ImRaii.Child("ScrollableSection"u8, ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoMove)) { - var itemsPerRow = (int)MathF.Floor( - ImGui.GetContentRegionMax().X / (this.iconSize.X + ImGui.GetStyle().ItemSpacing.X)); - var itemHeight = this.iconSize.Y + ImGui.GetStyle().ItemSpacing.Y; + if (child.Success) + { + var itemsPerRow = (int)MathF.Floor(ImGui.GetContentRegionMax().X / (this.iconSize.X + ImGui.GetStyle().ItemSpacing.X)); + var itemHeight = this.iconSize.Y + ImGui.GetStyle().ItemSpacing.Y; - ImGuiClip.ClippedDraw(this.valueRange!, this.DrawIcon, itemsPerRow, itemHeight); + ImGuiClip.ClippedDraw(this.valueRange!, this.DrawIcon, itemsPerRow, itemHeight); + } } - ImGui.EndChild(); - this.ProcessMouseDragging(); } } @@ -118,16 +120,16 @@ public class IconBrowserWidget : IDataWindowWidget { ImGui.Columns(2); - ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.InputInt("##StartRange"u8, ref this.startRange, 0, 0)) + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputInt("##StartRange"u8, ref this.startRange)) { this.startRange = Math.Clamp(this.startRange, 0, MaxIconId); this.valueRange = null; } ImGui.NextColumn(); - ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.InputInt("##StopRange"u8, ref this.stopRange, 0, 0)) + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputInt("##StopRange"u8, ref this.stopRange)) { this.stopRange = Math.Clamp(this.stopRange, 0, MaxIconId); this.valueRange = null; @@ -151,6 +153,10 @@ public class IconBrowserWidget : IDataWindowWidget var texm = Service.Get(); var cursor = ImGui.GetCursorScreenPos(); + var white = ImGui.GetColorU32(ImGuiColors.DalamudWhite); + var red = ImGui.GetColorU32(ImGuiColors.DalamudRed); + var drawList = ImGui.GetWindowDrawList(); + if (texm.Shared.GetFromGameIcon(iconId).TryGetWrap(out var texture, out var exc)) { ImGui.Image(texture.Handle, this.iconSize); @@ -158,21 +164,17 @@ public class IconBrowserWidget : IDataWindowWidget // If we have the option to show a tooltip image, draw the image, but make sure it's not too big. if (ImGui.IsItemHovered() && this.showTooltipImage) { - ImGui.BeginTooltip(); + using var tooltip = ImRaii.Tooltip(); var scale = GetImageScaleFactor(texture); var textSize = ImGui.CalcTextSize(iconId.ToString()); - ImGui.SetCursorPosX( - texture.Size.X * scale / 2.0f - textSize.X / 2.0f + ImGui.GetStyle().FramePadding.X * 2.0f); + ImGui.SetCursorPosX((texture.Size.X * scale / 2.0f - (textSize.X / 2.0f)) + (ImGui.GetStyle().FramePadding.X * 2.0f)); ImGui.Text(iconId.ToString()); ImGui.Image(texture.Handle, texture.Size * scale); - ImGui.EndTooltip(); } - - // else, just draw the iconId. - else if (ImGui.IsItemHovered()) + else if (ImGui.IsItemHovered()) // else, just draw the iconId. { ImGui.SetTooltip(iconId.ToString()); } @@ -185,10 +187,7 @@ public class IconBrowserWidget : IDataWindowWidget Task.FromResult(texture.CreateWrapSharingLowLevelResource())); } - ImGui.GetWindowDrawList().AddRect( - cursor, - cursor + this.iconSize, - ImGui.GetColorU32(ImGuiColors.DalamudWhite)); + drawList.AddRect(cursor, cursor + this.iconSize, white); } else if (exc is not null) { @@ -197,19 +196,13 @@ public class IconBrowserWidget : IDataWindowWidget { var iconText = FontAwesomeIcon.Ban.ToIconString(); var textSize = ImGui.CalcTextSize(iconText); - ImGui.GetWindowDrawList().AddText( - cursor + ((this.iconSize - textSize) / 2), - ImGui.GetColorU32(ImGuiColors.DalamudRed), - iconText); + drawList.AddText(cursor + ((this.iconSize - textSize) / 2), red, iconText); } if (ImGui.IsItemHovered()) ImGui.SetTooltip($"{iconId}\n{exc}"); - ImGui.GetWindowDrawList().AddRect( - cursor, - cursor + this.iconSize, - ImGui.GetColorU32(ImGuiColors.DalamudRed)); + drawList.AddRect(cursor, cursor + this.iconSize, red); } else { @@ -218,18 +211,12 @@ public class IconBrowserWidget : IDataWindowWidget ImGui.Dummy(this.iconSize); var textSize = ImGui.CalcTextSize(text); - ImGui.GetWindowDrawList().AddText( - cursor + ((this.iconSize - textSize) / 2), - color, - text); + drawList.AddText(cursor + ((this.iconSize - textSize) / 2), color, text); if (ImGui.IsItemHovered()) ImGui.SetTooltip(iconId.ToString()); - ImGui.GetWindowDrawList().AddRect( - cursor, - cursor + this.iconSize, - color); + drawList.AddRect(cursor, cursor + this.iconSize, color); } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 4327fa12f..2581f902d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -54,8 +54,7 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Separator(); - ImGui.Text( - $"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); + ImGui.Text($"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); ImGui.Separator(); @@ -79,45 +78,24 @@ internal class ImGuiWidget : IDataWindowWidget switch (this.notificationTemplate.IconInt) { case 1 or 2: - ImGui.InputText( - "Icon Text##iconText"u8, - ref this.notificationTemplate.IconText, - 255); + ImGui.InputText("Icon Text##iconText"u8, ref this.notificationTemplate.IconText, 255); break; case 5 or 6: - ImGui.Combo( - "Asset##iconAssetCombo", - ref this.notificationTemplate.IconAssetInt, - NotificationTemplate.AssetSources); + ImGui.Combo("Asset##iconAssetCombo", ref this.notificationTemplate.IconAssetInt, NotificationTemplate.AssetSources); break; case 3 or 7: - ImGui.InputText( - "Game Path##iconText"u8, - ref this.notificationTemplate.IconText, - 255); + ImGui.InputText("Game Path##iconText"u8, ref this.notificationTemplate.IconText, 255); break; case 4 or 8: - ImGui.InputText( - "File Path##iconText"u8, - ref this.notificationTemplate.IconText, - 255); + ImGui.InputText("File Path##iconText"u8, ref this.notificationTemplate.IconText, 255); break; } - ImGui.Combo( - "Initial Duration", - ref this.notificationTemplate.InitialDurationInt, - NotificationTemplate.InitialDurationTitles); + ImGui.Combo("Initial Duration", ref this.notificationTemplate.InitialDurationInt, NotificationTemplate.InitialDurationTitles); - ImGui.Combo( - "Extension Duration", - ref this.notificationTemplate.HoverExtendDurationInt, - NotificationTemplate.HoverExtendDurationTitles); + ImGui.Combo("Extension Duration", ref this.notificationTemplate.HoverExtendDurationInt, NotificationTemplate.HoverExtendDurationTitles); - ImGui.Combo( - "Progress", - ref this.notificationTemplate.ProgressMode, - NotificationTemplate.ProgressModeTitles); + ImGui.Combo("Progress", ref this.notificationTemplate.ProgressMode, NotificationTemplate.ProgressModeTitles); ImGui.Checkbox("Respect UI Hidden"u8, ref this.notificationTemplate.RespectUiHidden); @@ -127,14 +105,11 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Checkbox("User Dismissable"u8, ref this.notificationTemplate.UserDismissable); - ImGui.Checkbox( - "Action Bar (always on if not user dismissable for the example)"u8, - ref this.notificationTemplate.ActionBar); + ImGui.Checkbox("Action Bar (always on if not user dismissable for the example)"u8, ref this.notificationTemplate.ActionBar); if (ImGui.Button("Add notification"u8)) { - var text = - "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + var text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; NewRandom(out var title, out var type, out var progress); if (this.notificationTemplate.ManualTitle) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs index f5b26c04c..818e3fabc 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs @@ -22,6 +22,11 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class InventoryWidget : IDataWindowWidget { + private const ImGuiTableFlags TableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | + ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoSavedSettings; + + private const ImGuiTableFlags InnerTableFlags = ImGuiTableFlags.BordersInner | ImGuiTableFlags.NoSavedSettings; + private DataManager dataManager; private TextureManager textureManager; private GameInventoryType? selectedInventoryType = GameInventoryType.Inventory1; @@ -64,7 +69,7 @@ internal class InventoryWidget : IDataWindowWidget private unsafe void DrawInventoryTypeList() { - using var table = ImRaii.Table("InventoryTypeTable"u8, 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoSavedSettings, new Vector2(300, -1)); + using var table = ImRaii.Table("InventoryTypeTable"u8, 2, TableFlags, new Vector2(300, -1)); if (!table) return; ImGui.TableSetupColumn("Type"u8); @@ -107,7 +112,7 @@ internal class InventoryWidget : IDataWindowWidget } } - private unsafe void DrawInventoryType(GameInventoryType inventoryType) + private void DrawInventoryType(GameInventoryType inventoryType) { var items = GameInventoryItem.GetReadOnlySpanOfInventory(inventoryType); if (items.IsEmpty) @@ -116,8 +121,9 @@ internal class InventoryWidget : IDataWindowWidget return; } - using var itemTable = ImRaii.Table("InventoryItemTable"u8, 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoSavedSettings); + using var itemTable = ImRaii.Table("InventoryItemTable"u8, 4, TableFlags); if (!itemTable) return; + ImGui.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 40); ImGui.TableSetupColumn("ItemId"u8, ImGuiTableColumnFlags.WidthFixed, 70); ImGui.TableSetupColumn("Quantity"u8, ImGuiTableColumnFlags.WidthFixed, 70); @@ -129,7 +135,7 @@ internal class InventoryWidget : IDataWindowWidget { var item = items[slotIndex]; - using var disableditem = ImRaii.Disabled(item.ItemId == 0); + using var disabledItem = ImRaii.Disabled(item.ItemId == 0); ImGui.TableNextRow(); ImGui.TableNextColumn(); // Slot @@ -154,11 +160,11 @@ internal class InventoryWidget : IDataWindowWidget if (ImGui.IsItemHovered()) { ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); - ImGui.BeginTooltip(); + + using var tooltip = ImRaii.Tooltip(); ImGui.Text("Click to copy IconId"u8); ImGui.Text($"ID: {iconId} – Size: {texture.Width}x{texture.Height}"); ImGui.Image(texture.Handle, new(texture.Width, texture.Height)); - ImGui.EndTooltip(); } if (ImGui.IsItemClicked()) @@ -169,7 +175,7 @@ internal class InventoryWidget : IDataWindowWidget using var itemNameColor = ImRaii.PushColor(ImGuiCol.Text, this.GetItemRarityColor(item.ItemId)); using var node = ImRaii.TreeNode($"{itemName}###{inventoryType}_{slotIndex}", ImGuiTreeNodeFlags.SpanAvailWidth); - itemNameColor.Dispose(); + itemNameColor.Pop(); using (var contextMenu = ImRaii.ContextPopupItem($"{inventoryType}_{slotIndex}_ContextMenu")) { @@ -184,7 +190,7 @@ internal class InventoryWidget : IDataWindowWidget if (!node) continue; - using var itemInfoTable = ImRaii.Table($"{inventoryType}_{slotIndex}_Table", 2, ImGuiTableFlags.BordersInner | ImGuiTableFlags.NoSavedSettings); + using var itemInfoTable = ImRaii.Table($"{inventoryType}_{slotIndex}_Table", 2, InnerTableFlags); if (!itemInfoTable) continue; ImGui.TableSetupColumn("Name"u8, ImGuiTableColumnFlags.WidthFixed, 150); @@ -266,7 +272,7 @@ internal class InventoryWidget : IDataWindowWidget ImGui.Text("Stains"u8); ImGui.TableNextColumn(); - using var stainTable = ImRaii.Table($"{inventoryType}_{slotIndex}_StainTable", 2, ImGuiTableFlags.BordersInner | ImGuiTableFlags.NoSavedSettings); + using var stainTable = ImRaii.Table($"{inventoryType}_{slotIndex}_StainTable", 2, InnerTableFlags); if (!stainTable) continue; ImGui.TableSetupColumn("Stain Id"u8, ImGuiTableColumnFlags.WidthFixed, 80); @@ -287,7 +293,7 @@ internal class InventoryWidget : IDataWindowWidget ImGui.Text("Materia"u8); ImGui.TableNextColumn(); - using var materiaTable = ImRaii.Table($"{inventoryType}_{slotIndex}_MateriaTable", 2, ImGuiTableFlags.BordersInner | ImGuiTableFlags.NoSavedSettings); + using var materiaTable = ImRaii.Table($"{inventoryType}_{slotIndex}_MateriaTable", 2, InnerTableFlags); if (!materiaTable) continue; ImGui.TableSetupColumn("Materia Id"u8, ImGuiTableColumnFlags.WidthFixed, 80); @@ -313,10 +319,12 @@ internal class InventoryWidget : IDataWindowWidget private uint GetItemRarityColor(uint itemId, bool isEdgeColor = false) { - if (ItemUtil.IsEventItem(itemId)) + var normalized = ItemUtil.GetBaseId(itemId); + + if (normalized.Kind == ItemKind.EventItem) return isEdgeColor ? 0xFF000000 : 0xFFFFFFFF; - if (!this.dataManager.Excel.GetSheet().TryGetRow(ItemUtil.GetBaseId(itemId).ItemId, out var item)) + if (!this.dataManager.Excel.GetSheet().TryGetRow(normalized.ItemId, out var item)) return isEdgeColor ? 0xFF000000 : 0xFFFFFFFF; var rowId = ItemUtil.GetItemRarityColorType(item.RowId, isEdgeColor); @@ -327,18 +335,12 @@ internal class InventoryWidget : IDataWindowWidget private uint GetItemIconId(uint itemId) { + var normalized = ItemUtil.GetBaseId(itemId); + // EventItem - if (ItemUtil.IsEventItem(itemId)) + if (normalized.Kind == ItemKind.EventItem) return this.dataManager.Excel.GetSheet().TryGetRow(itemId, out var eventItem) ? eventItem.Icon : 0u; - // HighQuality - if (ItemUtil.IsHighQuality(itemId)) - itemId -= 1_000_000; - - // Collectible - if (ItemUtil.IsCollectible(itemId)) - itemId -= 500_000; - - return this.dataManager.Excel.GetSheet().TryGetRow(itemId, out var item) ? item.Icon : 0u; + return this.dataManager.Excel.GetSheet().TryGetRow(normalized.ItemId, out var item) ? item.Icon : 0u; } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs index 1b45b58fe..d3b021f7a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -29,6 +30,7 @@ internal class KeyStateWidget : IDataWindowWidget { var keyState = Service.Get(); + // TODO: Use table instead of columns ImGui.Columns(4); var i = 0; @@ -37,11 +39,10 @@ internal class KeyStateWidget : IDataWindowWidget var code = (int)vkCode; var value = keyState[code]; - ImGui.PushStyleColor(ImGuiCol.Text, value ? ImGuiColors.HealerGreen : ImGuiColors.DPSRed); - - ImGui.Text($"{vkCode} ({code})"); - - ImGui.PopStyleColor(); + using (ImRaii.PushColor(ImGuiCol.Text, value ? ImGuiColors.HealerGreen : ImGuiColors.DPSRed)) + { + ImGui.Text($"{vkCode} ({code})"); + } i++; if (i % 24 == 0) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs index 382c42f91..a74c5f3d3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs @@ -15,6 +15,8 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class MarketBoardWidget : IDataWindowWidget { + private const ImGuiTableFlags TableFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg; + private readonly ConcurrentQueue<(IMarketBoardHistory MarketBoardHistory, IMarketBoardHistoryListing Listing)> marketBoardHistoryQueue = new(); private readonly ConcurrentQueue<(IMarketBoardCurrentOfferings MarketBoardCurrentOfferings, IMarketBoardItemListing Listing)> marketBoardCurrentOfferingsQueue = new(); private readonly ConcurrentQueue marketBoardPurchasesQueue = new(); @@ -99,49 +101,47 @@ internal class MarketBoardWidget : IDataWindowWidget this.marketBoardHistoryQueue.Clear(); } - using (var tabBar = ImRaii.TabBar("marketTabs"u8)) + using var tabBar = ImRaii.TabBar("marketTabs"u8); + if (!tabBar.Success) + return; + + using (var tabItem = ImRaii.TabItem("History"u8)) { - if (tabBar) + if (tabItem) { - using (var tabItem = ImRaii.TabItem("History"u8)) - { - if (tabItem) - { - ImGuiTable.DrawTable(string.Empty, this.marketBoardHistoryQueue, this.DrawMarketBoardHistory, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Item ID", "Quantity", "Is HQ?", "Sale Price", "Buyer Name", "Purchase Time"); - } - } + ImGuiTable.DrawTable("history-table", this.marketBoardHistoryQueue, this.DrawMarketBoardHistory, TableFlags, "Item ID", "Quantity", "Is HQ?", "Sale Price", "Buyer Name", "Purchase Time"); + } + } - using (var tabItem = ImRaii.TabItem("Offerings"u8)) - { - if (tabItem) - { - ImGuiTable.DrawTable(string.Empty, this.marketBoardCurrentOfferingsQueue, this.DrawMarketBoardCurrentOfferings, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Item ID", "Quantity", "Is HQ?", "Price Per Unit", "Retainer Name"); - } - } + using (var tabItem = ImRaii.TabItem("Offerings"u8)) + { + if (tabItem) + { + ImGuiTable.DrawTable("offerings-table", this.marketBoardCurrentOfferingsQueue, this.DrawMarketBoardCurrentOfferings, TableFlags, "Item ID", "Quantity", "Is HQ?", "Price Per Unit", "Retainer Name"); + } + } - using (var tabItem = ImRaii.TabItem("Purchases"u8)) - { - if (tabItem) - { - ImGuiTable.DrawTable(string.Empty, this.marketBoardPurchasesQueue, this.DrawMarketBoardPurchases, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Item ID", "Quantity"); - } - } + using (var tabItem = ImRaii.TabItem("Purchases"u8)) + { + if (tabItem) + { + ImGuiTable.DrawTable("purchases-table", this.marketBoardPurchasesQueue, this.DrawMarketBoardPurchases, TableFlags, "Item ID", "Quantity"); + } + } - using (var tabItem = ImRaii.TabItem("Purchase Requests"u8)) - { - if (tabItem) - { - ImGuiTable.DrawTable(string.Empty, this.marketBoardPurchaseRequestsQueue, this.DrawMarketBoardPurchaseRequests, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Item ID", "Is HQ?", "Quantity", "Price Per Unit", "Total Tax", "City ID", "Listing ID", "Retainer ID"); - } - } + using (var tabItem = ImRaii.TabItem("Purchase Requests"u8)) + { + if (tabItem) + { + ImGuiTable.DrawTable("requests-table", this.marketBoardPurchaseRequestsQueue, this.DrawMarketBoardPurchaseRequests, TableFlags, "Item ID", "Is HQ?", "Quantity", "Price Per Unit", "Total Tax", "City ID", "Listing ID", "Retainer ID"); + } + } - using (var tabItem = ImRaii.TabItem("Taxes"u8)) - { - if (tabItem) - { - ImGuiTable.DrawTable(string.Empty, this.marketTaxRatesQueue, this.DrawMarketTaxRates, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Uldah", "Limsa Lominsa", "Gridania", "Ishgard", "Kugane", "Crystarium", "Sharlayan", "Tuliyollal", "Valid Until"); - } - } + using (var tabItem = ImRaii.TabItem("Taxes"u8)) + { + if (tabItem) + { + ImGuiTable.DrawTable("taxes-table", this.marketTaxRatesQueue, this.DrawMarketTaxRates, TableFlags, "Uldah", "Limsa Lominsa", "Gridania", "Ishgard", "Kugane", "Crystarium", "Sharlayan", "Tuliyollal", "Valid Until"); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 7761a18b4..ae173578a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -209,7 +209,7 @@ internal class NetworkMonitorWidget : IDataWindowWidget private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId) #pragma warning restore SA1313 { - public readonly IReadOnlyList Data = Array.Empty(); + public readonly IReadOnlyList Data = []; public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr) : this(opCode, direction, sourceActorId, targetActorId) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs index eac8b5a52..968254ceb 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs @@ -55,8 +55,8 @@ internal class NounProcessorWidget : IDataWindowWidget private ClientLanguage[] languages = []; private string[] languageNames = []; - private int selectedSheetNameIndex = 0; - private int selectedLanguageIndex = 0; + private int selectedSheetNameIndex; + private int selectedLanguageIndex; private int rowId = 1; private int amount = 1; @@ -84,7 +84,6 @@ internal class NounProcessorWidget : IDataWindowWidget { var nounProcessor = Service.Get(); var dataManager = Service.Get(); - var clientState = Service.Get(); var sheetType = NounSheets.ElementAt(this.selectedSheetNameIndex); var language = this.languages[this.selectedLanguageIndex]; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index dd5dc7472..6a6c0f8be 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -14,6 +14,11 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class ObjectTableWidget : IDataWindowWidget { + private const ImGuiWindowFlags CharacterWindowFlags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoMouseInputs | ImGuiWindowFlags.NoDocking | + ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNav; + private bool resolveGameData; private bool drawCharacters; private float maxCharaDrawDistance = 20.0f; @@ -49,74 +54,66 @@ internal class ObjectTableWidget : IDataWindowWidget if (objectTable.LocalPlayer == null) { ImGui.Text("LocalPlayer null."u8); + return; } - else if (clientState.IsPvPExcludingDen) + + if (clientState.IsPvPExcludingDen) { ImGui.Text("Cannot access object table while in PvP."u8); + return; } - else + + stateString += $"ObjectTableLen: {objectTable.Length}\n"; + stateString += $"LocalPlayerName: {playerState.CharacterName}\n"; + stateString += $"CurrentWorldName: {(this.resolveGameData ? playerState.CurrentWorld.ValueNullable?.Name : playerState.CurrentWorld.RowId.ToString())}\n"; + stateString += $"HomeWorldName: {(this.resolveGameData ? playerState.HomeWorld.ValueNullable?.Name : playerState.HomeWorld.RowId.ToString())}\n"; + stateString += $"LocalCID: {playerState.ContentId:X}\n"; + stateString += $"LastLinkedItem: {chatGui.LastLinkedItemId}\n"; + stateString += $"TerritoryType: {clientState.TerritoryType}\n\n"; + + ImGui.Text(stateString); + + ImGui.Checkbox("Draw characters on screen"u8, ref this.drawCharacters); + ImGui.SliderFloat("Draw Distance"u8, ref this.maxCharaDrawDistance, 2f, 40f); + + for (var i = 0; i < objectTable.Length; i++) { - stateString += $"ObjectTableLen: {objectTable.Length}\n"; - stateString += $"LocalPlayerName: {playerState.CharacterName}\n"; - stateString += $"CurrentWorldName: {(this.resolveGameData ? playerState.CurrentWorld.ValueNullable?.Name : playerState.CurrentWorld.RowId.ToString())}\n"; - stateString += $"HomeWorldName: {(this.resolveGameData ? playerState.HomeWorld.ValueNullable?.Name : playerState.HomeWorld.RowId.ToString())}\n"; - stateString += $"LocalCID: {playerState.ContentId:X}\n"; - stateString += $"LastLinkedItem: {chatGui.LastLinkedItemId}\n"; - stateString += $"TerritoryType: {clientState.TerritoryType}\n\n"; + var obj = objectTable[i]; - ImGui.Text(stateString); + if (obj == null) + continue; - ImGui.Checkbox("Draw characters on screen"u8, ref this.drawCharacters); - ImGui.SliderFloat("Draw Distance"u8, ref this.maxCharaDrawDistance, 2f, 40f); + Util.PrintGameObject(obj, i.ToString(), this.resolveGameData); - for (var i = 0; i < objectTable.Length; i++) + if (this.drawCharacters && gameGui.WorldToScreen(obj.Position, out var screenCoords)) { - var obj = objectTable[i]; + // So, while WorldToScreen will return false if the point is off of game client screen, to + // to avoid performance issues, we have to manually determine if creating a window would + // produce a new viewport, and skip rendering it if so + var objectText = $"{obj.Address:X}:{obj.GameObjectId:X}[{i}] - {obj.ObjectKind} - {obj.Name}"; - if (obj == null) + var screenPos = ImGui.GetMainViewport().Pos; + var screenSize = ImGui.GetMainViewport().Size; + + var windowSize = ImGui.CalcTextSize(objectText); + + // Add some extra safety padding + windowSize.X += ImGui.GetStyle().WindowPadding.X + 10; + windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10; + + if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X || + screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y) continue; - Util.PrintGameObject(obj, i.ToString(), this.resolveGameData); + if (obj.YalmDistanceX > this.maxCharaDrawDistance) + continue; - if (this.drawCharacters && gameGui.WorldToScreen(obj.Position, out var screenCoords)) - { - // So, while WorldToScreen will return false if the point is off of game client screen, to - // to avoid performance issues, we have to manually determine if creating a window would - // produce a new viewport, and skip rendering it if so - var objectText = $"{obj.Address.ToInt64():X}:{obj.GameObjectId:X}[{i}] - {obj.ObjectKind} - {obj.Name}"; + ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y)); + ImGui.SetNextWindowBgAlpha(Math.Max(1f - (obj.YalmDistanceX / this.maxCharaDrawDistance), 0.2f)); - var screenPos = ImGui.GetMainViewport().Pos; - var screenSize = ImGui.GetMainViewport().Size; - - var windowSize = ImGui.CalcTextSize(objectText); - - // Add some extra safety padding - windowSize.X += ImGui.GetStyle().WindowPadding.X + 10; - windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10; - - if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X || - screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y) - continue; - - if (obj.YalmDistanceX > this.maxCharaDrawDistance) - continue; - - ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y)); - - ImGui.SetNextWindowBgAlpha(Math.Max(1f - (obj.YalmDistanceX / this.maxCharaDrawDistance), 0.2f)); - if (ImGui.Begin( - $"Actor{i}##ActorWindow{i}", - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoSavedSettings | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoMouseInputs | - ImGuiWindowFlags.NoDocking | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoNav)) - ImGui.Text(objectText); - ImGui.End(); - } + if (ImGui.Begin($"Actor{i}##ActorWindow{i}", CharacterWindowFlags)) + ImGui.Text(objectText); + ImGui.End(); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs index 4033e4f41..597078f21 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs @@ -33,9 +33,9 @@ internal class PartyListWidget : IDataWindowWidget ImGui.Checkbox("Resolve GameData"u8, ref this.resolveGameData); - ImGui.Text($"GroupManager: {partyList.GroupManagerAddress.ToInt64():X}"); - ImGui.Text($"GroupList: {partyList.GroupListAddress.ToInt64():X}"); - ImGui.Text($"AllianceList: {partyList.AllianceListAddress.ToInt64():X}"); + ImGui.Text($"GroupManager: {partyList.GroupManagerAddress:X}"); + ImGui.Text($"GroupList: {partyList.GroupListAddress:X}"); + ImGui.Text($"AllianceList: {partyList.AllianceListAddress:X}"); ImGui.Text($"{partyList.Length} Members"); @@ -48,7 +48,7 @@ internal class PartyListWidget : IDataWindowWidget continue; } - ImGui.Text($"[{i}] {member.Address.ToInt64():X} - {member.Name} - {member.GameObject?.GameObjectId}"); + ImGui.Text($"[{i}] {member.Address:X} - {member.Name} - {member.GameObject?.GameObjectId ?? 0}"); if (this.resolveGameData) { var actor = member.GameObject; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index d43d3b7b2..a11bc3719 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -38,6 +38,11 @@ internal class SeStringCreatorWidget : IDataWindowWidget { private const LinkMacroPayloadType DalamudLinkType = (LinkMacroPayloadType)Payload.EmbeddedInfoType.DalamudLink - 1; + private const ImGuiTableFlags TableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | + ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoSavedSettings; + + private static readonly string[] TextEntryTypeOptions = ["String", "Macro", "Fixed"]; + private readonly Dictionary expressionNames = new() { { MacroCode.SetResetTime, ["Hour", "WeekDay"] }, @@ -191,7 +196,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget if (contentWidth != this.lastContentWidth) { var originalWidth = this.lastContentWidth != 0 ? this.lastContentWidth : contentWidth; - this.inputsWidth = this.inputsWidth / originalWidth * contentWidth; + this.inputsWidth = (this.inputsWidth / originalWidth) * contentWidth; this.lastContentWidth = contentWidth; } @@ -258,7 +263,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget using var tab = ImRaii.TabItem("Global Parameters"u8); if (!tab) return; - using var table = ImRaii.Table("GlobalParametersTable"u8, 5, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.NoSavedSettings); + using var table = ImRaii.Table("GlobalParametersTable"u8, 5, TableFlags); if (!table) return; ImGui.TableSetupColumn("Id"u8, ImGuiTableColumnFlags.WidthFixed, 40); @@ -541,18 +546,16 @@ internal class SeStringCreatorWidget : IDataWindowWidget ImGui.SameLine(); ImGui.SetNextItemWidth(90 * ImGuiHelpers.GlobalScale); - using (var dropdown = ImRaii.Combo("##Language"u8, this.language.ToString() ?? "Language...")) + using var dropdown = ImRaii.Combo("##Language"u8, this.language.ToString() ?? "Language..."); + if (dropdown) { - if (dropdown) + var values = Enum.GetValues().OrderBy(lang => lang.ToString()); + foreach (var value in values) { - var values = Enum.GetValues().OrderBy((ClientLanguage lang) => lang.ToString()); - foreach (var value in values) + if (ImGui.Selectable(Enum.GetName(value), value == this.language)) { - if (ImGui.Selectable(Enum.GetName(value), value == this.language)) - { - this.language = value; - this.UpdateInputString(); - } + this.language = value; + this.UpdateInputString(); } } } @@ -572,7 +575,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget try { var headerFile = dataManager.GameData.GetFile($"exd/{sheetName}.exh"); - if (headerFile.Header.Variant != ExcelVariant.Default) + if (headerFile == null || headerFile.Header.Variant != ExcelVariant.Default) return false; var sheet = dataManager.Excel.GetSheet(Language.English, sheetName); @@ -668,11 +671,10 @@ internal class SeStringCreatorWidget : IDataWindowWidget catch (Exception e) { ImGui.Text(e.Message); - return; } } - private unsafe void DrawInputs() + private void DrawInputs() { using var child = ImRaii.Child("Inputs"u8, new Vector2(this.inputsWidth, -1)); if (!child) return; @@ -688,8 +690,6 @@ internal class SeStringCreatorWidget : IDataWindowWidget var arrowUpButtonSize = this.GetIconButtonSize(FontAwesomeIcon.ArrowUp); var arrowDownButtonSize = this.GetIconButtonSize(FontAwesomeIcon.ArrowDown); - var trashButtonSize = this.GetIconButtonSize(FontAwesomeIcon.Trash); - var terminalButtonSize = this.GetIconButtonSize(FontAwesomeIcon.Terminal); var entryToRemove = -1; var entryToMoveUp = -1; @@ -706,7 +706,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget ImGui.TableNextColumn(); // Type var type = (int)entry.Type; ImGui.SetNextItemWidth(-1); - if (ImGui.Combo($"##Type{i}", ref type, ["String", "Macro", "Fixed"])) + if (ImGui.Combo($"##Type{i}", ref type, TextEntryTypeOptions)) { entry.Type = (TextEntryType)type; updateString |= true; @@ -798,7 +798,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget } } - private unsafe void UpdateInputString(bool resetLocalParameters = true) + private void UpdateInputString(bool resetLocalParameters = true) { using var rssb = new RentedSeStringBuilder(); @@ -1022,14 +1022,14 @@ internal class SeStringCreatorWidget : IDataWindowWidget if (macroCode is MacroCode.JaNoun or MacroCode.EnNoun or MacroCode.DeNoun or MacroCode.FrNoun && exprIdx == 1) { - var language = macroCode switch + var macroLanguage = macroCode switch { MacroCode.JaNoun => ClientLanguage.Japanese, MacroCode.DeNoun => ClientLanguage.German, MacroCode.FrNoun => ClientLanguage.French, _ => ClientLanguage.English, }; - var articleTypeEnumType = language switch + var articleTypeEnumType = macroLanguage switch { ClientLanguage.Japanese => typeof(JapaneseArticleType), ClientLanguage.German => typeof(GermanArticleType), @@ -1208,12 +1208,10 @@ internal class SeStringCreatorWidget : IDataWindowWidget if (expressionType == (int)ExpressionType.LocalNumber) { parameters[index] = new SeStringParameter(0); - return; } else if (expressionType == (int)ExpressionType.LocalString) { parameters[index] = new SeStringParameter(string.Empty); - return; } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs index 3ddc2a888..a4351248b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -66,11 +66,10 @@ internal class ServicesWidget : IDataWindowWidget var margin = ImGui.CalcTextSize("W\nW\nW"u8); var rowHeight = cellPad.Y * 3; var width = ImGui.GetContentRegionAvail().X; - if (ImGui.BeginChild( - "dependency-graph"u8, - new(width, (this.dependencyNodes.Count * (rowHeight + margin.Y)) + cellPad.Y), - false, - ImGuiWindowFlags.HorizontalScrollbar)) + var childSize = new Vector2(width, (this.dependencyNodes.Count * (rowHeight + margin.Y)) + cellPad.Y); + + using var child = ImRaii.Child("dependency-graph"u8, childSize, false, ImGuiWindowFlags.HorizontalScrollbar); + if (child.Success) { const uint rectBaseBorderColor = 0xFFFFFFFF; const uint rectHoverFillColor = 0xFF404040; @@ -118,10 +117,8 @@ internal class ServicesWidget : IDataWindowWidget hoveredNode = node; if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { - if (this.selectedNodes.Contains(node.Type)) + if (!this.selectedNodes.Add(node.Type)) this.selectedNodes.Remove(node.Type); - else - this.selectedNodes.Add(node.Type); } } @@ -195,13 +192,11 @@ internal class ServicesWidget : IDataWindowWidget ImGui.SetTooltip(node.BlockingReason); ImGui.SetCursorPos((new Vector2(rc.X, rc.Y) - pos) + ((cellSize - textSize) / 2)); - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + using var pushedStyle = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); ImGui.Text(node.DisplayedName); ImGui.SameLine(); - ImGui.PushStyleColor(ImGuiCol.Text, node.TypeSuffixColor); + using var pushedColor = ImRaii.PushColor(ImGuiCol.Text, node.TypeSuffixColor); ImGui.Text(node.TypeSuffix); - ImGui.PopStyleVar(); - ImGui.PopStyleColor(); } } @@ -233,7 +228,6 @@ internal class ServicesWidget : IDataWindowWidget ImGui.SetCursorPos(default); ImGui.Dummy(new(maxRowWidth, this.dependencyNodes.Count * rowHeight)); - ImGui.EndChild(); } } From afa7b0c1f3d4330c1ec87bee17876978d46f30ab Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:30:58 +0100 Subject: [PATCH 430/477] Improve parameter verification logic in HookVerifier (#2596) * Improve parameter verification logic in HookVerifier Refactor HookVerifier to enhance parameter type checking and add utility methods for size calculations. * Reverse bool check * Fix type size check on return type * Fix non static member in static class * Fix compiler errors * Fix SizeOf calls * Fix IsStruct call * Cleanup some warnings --- .../Internal/Verification/HookVerifier.cs | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs index ad68ae38e..ebe6851ce 100644 --- a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -1,8 +1,13 @@ using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Logging.Internal; +using InteropGenerator.Runtime; + namespace Dalamud.Hooking.Internal.Verification; /// @@ -19,11 +24,13 @@ internal static class HookVerifier new( "ActorControlSelf", "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", - typeof(ActorControlSelfDelegate), + typeof(ActorControlSelfDelegate), // TODO: change this to CS delegate "Signature changed in Patch 7.4") // 7.4 (new parameters) ]; - private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); + private static readonly string ClientStructsInteropNamespacePrefix = string.Join(".", nameof(FFXIVClientStructs), nameof(FFXIVClientStructs.Interop)); + + private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); // TODO: change this to CS delegate /// /// Initializes a new instance of the class. @@ -71,7 +78,7 @@ internal static class HookVerifier var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; // Compare Return Type - var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType; + var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType); // Compare Parameter Count var passedParams = passedInvoke.GetParameters(); @@ -86,7 +93,7 @@ internal static class HookVerifier // Compare Parameter Types for (var i = 0; i < passedParams.Length; i++) { - if (passedParams[i].ParameterType != enforcedParams[i].ParameterType) + if (!CheckParam(passedParams[i].ParameterType, enforcedParams[i].ParameterType)) { mismatch = true; break; @@ -100,6 +107,45 @@ internal static class HookVerifier } } + private static bool CheckParam(Type paramLeft, Type paramRight) + { + var sameType = paramLeft == paramRight; + return sameType || SizeOf(paramLeft) == SizeOf(paramRight); + } + + private static int SizeOf(Type type) + { + return type switch { + _ when type == typeof(sbyte) || type == typeof(byte) || type == typeof(bool) => 1, + _ when type == typeof(char) || type == typeof(short) || type == typeof(ushort) || type == typeof(Half) => 2, + _ when type == typeof(int) || type == typeof(uint) || type == typeof(float) => 4, + _ when type == typeof(long) || type == typeof(ulong) || type == typeof(double) || type.IsPointer || type.IsFunctionPointer || type.IsUnmanagedFunctionPointer || (type.Name == "Pointer`1" && type.Namespace.AsSpan().SequenceEqual(ClientStructsInteropNamespacePrefix)) || type == typeof(CStringPointer) => 8, + _ when type.Name.StartsWith("FixedSizeArray") => SizeOf(type.GetGenericArguments()[0]) * int.Parse(type.Name[14..type.Name.IndexOf('`')]), + _ when type.GetCustomAttribute() is { Length: var length } => SizeOf(type.GetGenericArguments()[0]) * length, + _ when IsStruct(type) && !type.IsGenericType && (type.StructLayoutAttribute?.Value ?? LayoutKind.Sequential) != LayoutKind.Sequential => type.StructLayoutAttribute?.Size ?? (int?)typeof(Unsafe).GetMethod("SizeOf")?.MakeGenericMethod(type).Invoke(null, null) ?? 0, + _ when type.IsEnum => SizeOf(Enum.GetUnderlyingType(type)), + _ when type.IsGenericType => Marshal.SizeOf(Activator.CreateInstance(type)!), + _ => GetSizeOf(type), + }; + } + + private static int GetSizeOf(Type type) + { + try + { + return Marshal.SizeOf(Activator.CreateInstance(type)!); + } + catch + { + return 0; + } + } + + private static bool IsStruct(Type type) + { + return type != typeof(decimal) && type is { IsValueType: true, IsPrimitive: false, IsEnum: false }; + } + private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message) { public nint Address { get; set; } From 3abf7bb00bc834be650f784a322870510245fefa Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 27 Jan 2026 17:35:55 +0100 Subject: [PATCH 431/477] Rework NetworkMonitorWidget, remove GameNetwork (#2593) * Rework NetworkMonitorWidget, remove GameNetwork * Rework packet filtering --- Dalamud/Game/Network/GameNetwork.cs | 147 -------- .../Network/GameNetworkAddressResolver.cs | 20 - .../Game/Network/Internal/NetworkHandlers.cs | 5 +- .../Game/Network/NetworkMessageDirection.cs | 17 - .../Data/Widgets/NetworkMonitorWidget.cs | 341 +++++++++++------- Dalamud/Plugin/Services/IGameNetwork.cs | 27 -- 6 files changed, 212 insertions(+), 345 deletions(-) delete mode 100644 Dalamud/Game/Network/GameNetwork.cs delete mode 100644 Dalamud/Game/Network/GameNetworkAddressResolver.cs delete mode 100644 Dalamud/Game/Network/NetworkMessageDirection.cs delete mode 100644 Dalamud/Plugin/Services/IGameNetwork.cs diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs deleted file mode 100644 index b8c91b235..000000000 --- a/Dalamud/Game/Network/GameNetwork.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Runtime.InteropServices; - -using Dalamud.Configuration.Internal; -using Dalamud.Hooking; -using Dalamud.Utility; - -using FFXIVClientStructs.FFXIV.Client.Network; - -using Serilog; - -namespace Dalamud.Game.Network; - -/// -/// This class handles interacting with game network events. -/// -[ServiceManager.EarlyLoadedService] -internal sealed unsafe class GameNetwork : IInternalDisposableService -{ - private readonly GameNetworkAddressResolver address; - private readonly Hook processZonePacketDownHook; - private readonly Hook processZonePacketUpHook; - - private readonly HitchDetector hitchDetectorUp; - private readonly HitchDetector hitchDetectorDown; - - [ServiceManager.ServiceDependency] - private readonly DalamudConfiguration configuration = Service.Get(); - - [ServiceManager.ServiceConstructor] - private unsafe GameNetwork(TargetSigScanner sigScanner) - { - this.hitchDetectorUp = new HitchDetector("GameNetworkUp", this.configuration.GameNetworkUpHitch); - this.hitchDetectorDown = new HitchDetector("GameNetworkDown", this.configuration.GameNetworkDownHitch); - - this.address = new GameNetworkAddressResolver(); - this.address.Setup(sigScanner); - - var onReceivePacketAddress = (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket; - - Log.Verbose("===== G A M E N E T W O R K ====="); - Log.Verbose($"OnReceivePacket address {Util.DescribeAddress(onReceivePacketAddress)}"); - Log.Verbose($"ProcessZonePacketUp address {Util.DescribeAddress(this.address.ProcessZonePacketUp)}"); - - this.processZonePacketDownHook = Hook.FromAddress(onReceivePacketAddress, this.ProcessZonePacketDownDetour); - this.processZonePacketUpHook = Hook.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour); - - this.processZonePacketDownHook.Enable(); - this.processZonePacketUpHook.Enable(); - } - - /// - /// The delegate type of a network message event. - /// - /// The pointer to the raw data. - /// The operation ID code. - /// The source actor ID. - /// The taret actor ID. - /// The direction of the packed. - public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction); - - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate byte ProcessZonePacketUpDelegate(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4); - - /// - /// Event that is called when a network message is sent/received. - /// - public event OnNetworkMessageDelegate? NetworkMessage; - - /// - void IInternalDisposableService.DisposeService() - { - this.processZonePacketDownHook.Dispose(); - this.processZonePacketUpHook.Dispose(); - } - - private void ProcessZonePacketDownDetour(PacketDispatcher* dispatcher, uint targetId, IntPtr dataPtr) - { - this.hitchDetectorDown.Start(); - - // Go back 0x10 to get back to the start of the packet header - dataPtr -= 0x10; - - foreach (var d in Delegate.EnumerateInvocationList(this.NetworkMessage)) - { - try - { - d.Invoke( - dataPtr + 0x20, - (ushort)Marshal.ReadInt16(dataPtr, 0x12), - 0, - targetId, - NetworkMessageDirection.ZoneDown); - } - catch (Exception ex) - { - string header; - try - { - var data = new byte[32]; - Marshal.Copy(dataPtr, data, 0, 32); - header = BitConverter.ToString(data); - } - catch (Exception) - { - header = "failed"; - } - - Log.Error(ex, "Exception on ProcessZonePacketDown hook. Header: " + header); - } - } - - this.processZonePacketDownHook.Original(dispatcher, targetId, dataPtr + 0x10); - this.hitchDetectorDown.Stop(); - } - - private byte ProcessZonePacketUpDetour(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4) - { - this.hitchDetectorUp.Start(); - - try - { - // Call events - // TODO: Implement actor IDs - this.NetworkMessage?.Invoke(dataPtr + 0x20, (ushort)Marshal.ReadInt16(dataPtr), 0x0, 0x0, NetworkMessageDirection.ZoneUp); - } - catch (Exception ex) - { - string header; - try - { - var data = new byte[32]; - Marshal.Copy(dataPtr, data, 0, 32); - header = BitConverter.ToString(data); - } - catch (Exception) - { - header = "failed"; - } - - Log.Error(ex, "Exception on ProcessZonePacketUp hook. Header: " + header); - } - - this.hitchDetectorUp.Stop(); - - return this.processZonePacketUpHook.Original(a1, dataPtr, a3, a4); - } -} diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs deleted file mode 100644 index 48abc2d97..000000000 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Dalamud.Plugin.Services; - -namespace Dalamud.Game.Network; - -/// -/// The address resolver for the class. -/// -internal sealed class GameNetworkAddressResolver : BaseAddressResolver -{ - /// - /// Gets the address of the ProcessZonePacketUp method. - /// - public IntPtr ProcessZonePacketUp { get; private set; } - - /// - protected override void Setup64Bit(ISigScanner sig) - { - this.ProcessZonePacketUp = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 4C 89 64 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 70"); // unnamed in cs - } -} diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 5ca7da54a..d3a53b4f2 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -55,10 +55,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService private bool disposing; [ServiceManager.ServiceConstructor] - private NetworkHandlers( - GameNetwork gameNetwork, - TargetSigScanner sigScanner, - HappyHttpClient happyHttpClient) + private NetworkHandlers(TargetSigScanner sigScanner, HappyHttpClient happyHttpClient) { this.uploader = new UniversalisMarketBoardUploader(happyHttpClient); diff --git a/Dalamud/Game/Network/NetworkMessageDirection.cs b/Dalamud/Game/Network/NetworkMessageDirection.cs deleted file mode 100644 index 87cce5173..000000000 --- a/Dalamud/Game/Network/NetworkMessageDirection.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Dalamud.Game.Network; - -/// -/// This represents the direction of a network message. -/// -public enum NetworkMessageDirection -{ - /// - /// A zone down message. - /// - ZoneDown, - - /// - /// A zone up message. - /// - ZoneUp, -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index ae173578a..922b72717 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -1,44 +1,51 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; +using System.Threading; using Dalamud.Bindings.ImGui; -using Dalamud.Game.Network; -using Dalamud.Interface.Utility; +using Dalamud.Game; +using Dalamud.Hooking; +using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; -using Dalamud.Memory; -using ImGuiTable = Dalamud.Interface.Utility.ImGuiTable; +using FFXIVClientStructs.FFXIV.Application.Network; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.Network; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget to display the current packets. /// -internal class NetworkMonitorWidget : IDataWindowWidget +internal unsafe class NetworkMonitorWidget : IDataWindowWidget { private readonly ConcurrentQueue packets = new(); + private Hook? hookDown; + private Hook? hookUp; + private bool trackNetwork; - private int trackedPackets; - private Regex? trackedOpCodes; + private int trackedPackets = 20; + private ulong nextPacketIndex; private string filterString = string.Empty; - private Regex? untrackedOpCodes; - private string negativeFilterString = string.Empty; + private bool filterRecording = true; + private bool autoScroll = true; + private bool autoScrollPending; /// Finalizes an instance of the class. ~NetworkMonitorWidget() { - if (this.trackNetwork) - { - this.trackNetwork = false; - var network = Service.GetNullable(); - if (network != null) - { - network.NetworkMessage -= this.OnNetworkMessage; - } - } + this.hookDown?.Dispose(); + this.hookUp?.Dispose(); + } + + private delegate byte ZoneClientSendPacketDelegate(ZoneClient* thisPtr, nint packet, uint a3, uint a4, byte a5); + + private enum NetworkMessageDirection + { + ZoneDown, + ZoneUp, } /// @@ -53,31 +60,36 @@ internal class NetworkMonitorWidget : IDataWindowWidget /// public void Load() { - this.trackNetwork = false; - this.trackedPackets = 20; - this.trackedOpCodes = null; - this.filterString = string.Empty; - this.packets.Clear(); + this.hookDown = Hook.FromAddress( + (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket, + this.OnReceivePacketDetour); + + // TODO: switch to ZoneClient.SendPacket from CS + if (Service.Get().TryScanText("E8 ?? ?? ?? ?? 4C 8B 44 24 ?? E9", out var address)) + this.hookUp = Hook.FromAddress(address, this.SendPacketDetour); + this.Ready = true; } /// public void Draw() { - var network = Service.Get(); if (ImGui.Checkbox("Track Network Packets"u8, ref this.trackNetwork)) { if (this.trackNetwork) { - network.NetworkMessage += this.OnNetworkMessage; + this.nextPacketIndex = 0; + this.hookDown?.Enable(); + this.hookUp?.Enable(); } else { - network.NetworkMessage -= this.OnNetworkMessage; + this.hookDown?.Disable(); + this.hookUp?.Disable(); } } - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2); + ImGui.SetNextItemWidth(-1); if (ImGui.DragInt("Stored Number of Packets"u8, ref this.trackedPackets, 0.1f, 1, 512)) { this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512); @@ -88,131 +100,200 @@ internal class NetworkMonitorWidget : IDataWindowWidget this.packets.Clear(); } - this.DrawFilterInput(); - this.DrawNegativeFilterInput(); + ImGui.SameLine(); + ImGui.Checkbox("Auto-Scroll"u8, ref this.autoScroll); - ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "OpCode", "Hex", "Target", "Source", "Data"); - } + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight()) * 2); + ImGui.InputTextWithHint("##Filter"u8, "Filter OpCodes..."u8, ref this.filterString, 1024, ImGuiInputTextFlags.AutoSelectAll); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.Checkbox("##FilterRecording"u8, ref this.filterRecording); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Apply filter to incoming packets.\nUncheck to record all packets and filter the table instead."u8); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGuiComponents.HelpMarker("Enter OpCodes in a comma-separated list.\nRanges are supported. Exclude OpCodes with exclamation mark.\nExample: -400,!50-100,650,700-980,!941"); - private void DrawNetworkPacket(NetworkPacketData data) - { - ImGui.TableNextColumn(); - ImGui.Text(data.Direction.ToString()); + using var table = ImRaii.Table("NetworkMonitorTableV2"u8, 6, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.NoSavedSettings); + if (!table) return; - ImGui.TableNextColumn(); - ImGui.Text(data.OpCode.ToString()); + ImGui.TableSetupColumn("Index"u8, ImGuiTableColumnFlags.WidthFixed, 50); + ImGui.TableSetupColumn("Time"u8, ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Direction"u8, ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("OpCode"u8, ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("OpCode (Hex)"u8, ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Target EntityId"u8, ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); - ImGui.TableNextColumn(); - ImGui.Text($"0x{data.OpCode:X4}"); + var autoScrollDisabled = false; - ImGui.TableNextColumn(); - ImGui.Text(data.TargetActorId > 0 ? $"0x{data.TargetActorId:X}" : string.Empty); - - ImGui.TableNextColumn(); - ImGui.Text(data.SourceActorId > 0 ? $"0x{data.SourceActorId:X}" : string.Empty); - - ImGui.TableNextColumn(); - if (data.Data.Count > 0) + foreach (var packet in this.packets) { - ImGui.Text(string.Join(" ", data.Data.Select(b => b.ToString("X2")))); + if (!this.filterRecording && !this.IsFiltered(packet.OpCode)) + continue; + + ImGui.TableNextColumn(); + ImGui.Text(packet.Index.ToString()); + + ImGui.TableNextColumn(); + ImGui.Text(packet.Time.ToLongTimeString()); + + ImGui.TableNextColumn(); + ImGui.Text(packet.Direction.ToString()); + + ImGui.TableNextColumn(); + using (ImRaii.PushId(packet.Index.ToString())) + { + if (ImGui.SmallButton("X")) + { + if (!string.IsNullOrEmpty(this.filterString)) + this.filterString += ","; + + this.filterString += $"!{packet.OpCode}"; + } + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Filter OpCode"u8); + + autoScrollDisabled |= ImGui.IsItemHovered(); + + ImGui.SameLine(); + WidgetUtil.DrawCopyableText(packet.OpCode.ToString()); + autoScrollDisabled |= ImGui.IsItemHovered(); + + ImGui.TableNextColumn(); + WidgetUtil.DrawCopyableText($"0x{packet.OpCode:X3}"); + autoScrollDisabled |= ImGui.IsItemHovered(); + + ImGui.TableNextColumn(); + if (packet.TargetEntityId > 0) + { + WidgetUtil.DrawCopyableText($"{packet.TargetEntityId:X}"); + + var name = !string.IsNullOrEmpty(packet.TargetName) + ? packet.TargetName + : GetTargetName(packet.TargetEntityId); + + if (!string.IsNullOrEmpty(name)) + { + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.Text($"({name})"); + } + } } - else + + if (this.autoScroll && this.autoScrollPending && !autoScrollDisabled) { - ImGui.Dummy(ImGui.GetContentRegionAvail() with { Y = 0 }); + ImGui.SetScrollHereY(); + this.autoScrollPending = false; } } - private void DrawFilterInput() + private static string GetTargetName(uint targetId) { - var invalidRegEx = this.filterString.Length > 0 && this.trackedOpCodes == null; - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx); - using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (!ImGui.InputTextWithHint("##Filter"u8, "Regex Filter OpCodes..."u8, ref this.filterString, 1024)) - { + if (targetId == PlayerState.Instance()->EntityId) + return "Local Player"; + + var cachedName = NameCache.Instance()->GetNameByEntityId(targetId); + if (cachedName.HasValue) + return cachedName.ToString(); + + var obj = GameObjectManager.Instance()->Objects.GetObjectByEntityId(targetId); + if (obj != null) + return obj->NameString; + + return string.Empty; + } + + private void OnReceivePacketDetour(PacketDispatcher* thisPtr, uint targetId, nint packet) + { + var opCode = *(ushort*)(packet + 2); + var targetName = GetTargetName(targetId); + this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId, targetName)); + this.hookDown.OriginalDisposeSafe(thisPtr, targetId, packet); + } + + private byte SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, byte a5) + { + var opCode = *(ushort*)packet; + this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty)); + return this.hookUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5); + } + + private void RecordPacket(NetworkPacketData packet) + { + if (this.filterRecording && !this.IsFiltered(packet.OpCode)) return; + + this.packets.Enqueue(packet); + + while (this.packets.Count > this.trackedPackets) + { + this.packets.TryDequeue(out _); } - if (this.filterString.Length == 0) - { - this.trackedOpCodes = null; - } - else - { - try - { - this.trackedOpCodes = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture); - } - catch - { - this.trackedOpCodes = null; - } - } + this.autoScrollPending = true; } - private void DrawNegativeFilterInput() + private bool IsFiltered(ushort opcode) { - var invalidRegEx = this.negativeFilterString.Length > 0 && this.untrackedOpCodes == null; - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx); - using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (!ImGui.InputTextWithHint("##NegativeFilter"u8, "Regex Filter Against OpCodes..."u8, ref this.negativeFilterString, 1024)) - { - return; - } + var filterString = this.filterString.Replace(" ", string.Empty); - if (this.negativeFilterString.Length == 0) + if (filterString.Length == 0) + return true; + + try { - this.untrackedOpCodes = null; + var offset = 0; + var included = false; + var hasInclude = false; + + while (filterString.Length - offset > 0) + { + var remaining = filterString[offset..]; + + // find the end of the current entry + var entryEnd = remaining.IndexOf(','); + if (entryEnd == -1) + entryEnd = remaining.Length; + + var entry = filterString[offset..(offset + entryEnd)]; + var dash = entry.IndexOf('-'); + var isExcluded = entry.StartsWith('!'); + var startOffset = isExcluded ? 1 : 0; + + var entryMatch = dash == -1 + ? ushort.Parse(entry[startOffset..]) == opcode + : ((dash - startOffset == 0 || opcode >= ushort.Parse(entry[startOffset..dash])) + && (entry[(dash + 1)..].Length == 0 || opcode <= ushort.Parse(entry[(dash + 1)..]))); + + if (isExcluded) + { + if (entryMatch) + return false; + } + else + { + hasInclude = true; + included |= entryMatch; + } + + if (entryEnd == filterString.Length) + break; + + offset += entryEnd + 1; + } + + return !hasInclude || included; } - else + catch (Exception ex) { - try - { - this.untrackedOpCodes = new Regex(this.negativeFilterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture); - } - catch - { - this.untrackedOpCodes = null; - } + Serilog.Log.Error(ex, "Invalid filter string"); + return false; } } - private void OnNetworkMessage(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) - { - if ((this.trackedOpCodes == null || this.trackedOpCodes.IsMatch(this.OpCodeToString(opCode))) - && (this.untrackedOpCodes == null || !this.untrackedOpCodes.IsMatch(this.OpCodeToString(opCode)))) - { - this.packets.Enqueue(new NetworkPacketData(this, opCode, direction, sourceActorId, targetActorId, dataPtr)); - while (this.packets.Count > this.trackedPackets) - { - this.packets.TryDequeue(out _); - } - } - } - - private int GetSizeFromOpCode(ushort opCode) - => 0; - - /// Add known packet-name -> packet struct size associations here to copy the byte data for such packets. > - private int GetSizeFromName(string name) - => name switch - { - _ => 0, - }; - - /// The filter should find opCodes by number (decimal and hex) and name, if existing. - private string OpCodeToString(ushort opCode) - => $"{opCode}\0{opCode:X}"; - #pragma warning disable SA1313 - private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId) + private readonly record struct NetworkPacketData(ulong Index, DateTime Time, ushort OpCode, NetworkMessageDirection Direction, uint TargetEntityId, string TargetName); #pragma warning restore SA1313 - { - public readonly IReadOnlyList Data = []; - - public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr) - : this(opCode, direction, sourceActorId, targetActorId) - => this.Data = MemoryHelper.Read(dataPtr, widget.GetSizeFromOpCode(opCode), false); - } } diff --git a/Dalamud/Plugin/Services/IGameNetwork.cs b/Dalamud/Plugin/Services/IGameNetwork.cs deleted file mode 100644 index 4abf20834..000000000 --- a/Dalamud/Plugin/Services/IGameNetwork.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Dalamud.Game.Network; - -namespace Dalamud.Plugin.Services; - -/// -/// This class handles interacting with game network events. -/// -[Obsolete("Will be removed in a future release. Use packet handler hooks instead.", true)] -public interface IGameNetwork : IDalamudService -{ - // TODO(v9): we shouldn't be passing pointers to the actual data here - - /// - /// The delegate type of a network message event. - /// - /// The pointer to the raw data. - /// The operation ID code. - /// The source actor ID. - /// The taret actor ID. - /// The direction of the packed. - public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction); - - /// - /// Event that is called when a network message is sent/received. - /// - public event OnNetworkMessageDelegate NetworkMessage; -} From 5da79a7dbaa88a3c5695ec54b5882e19f393a5dc Mon Sep 17 00:00:00 2001 From: Limiana <5073202+Limiana@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:38:42 +0300 Subject: [PATCH 432/477] Use RowRef in ZoneInitEventArgs (#2540) * Try-catch packet read * Actually use RowRef instead --- Dalamud/Game/ClientState/ZoneInit.cs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Dalamud/Game/ClientState/ZoneInit.cs b/Dalamud/Game/ClientState/ZoneInit.cs index 7eb4576aa..7d6cda90f 100644 --- a/Dalamud/Game/ClientState/ZoneInit.cs +++ b/Dalamud/Game/ClientState/ZoneInit.cs @@ -3,8 +3,11 @@ using System.Text; using Dalamud.Data; +using Lumina.Excel; using Lumina.Excel.Sheets; +using Serilog; + namespace Dalamud.Game.ClientState; /// @@ -15,7 +18,7 @@ public class ZoneInitEventArgs : EventArgs /// /// Gets the territory type of the zone being entered. /// - public TerritoryType TerritoryType { get; private set; } + public RowRef TerritoryType { get; private set; } /// /// Gets the instance number of the zone, used when multiple copies of an area are active. @@ -25,17 +28,17 @@ public class ZoneInitEventArgs : EventArgs /// /// Gets the associated content finder condition for the zone, if any. /// - public ContentFinderCondition ContentFinderCondition { get; private set; } + public RowRef ContentFinderCondition { get; private set; } /// /// Gets the current weather in the zone upon entry. /// - public Weather Weather { get; private set; } + public RowRef Weather { get; private set; } /// /// Gets the set of active festivals in the zone. /// - public Festival[] ActiveFestivals { get; private set; } = []; + public RowRef[] ActiveFestivals { get; private set; } = []; /// /// Gets the phases corresponding to the active festivals. @@ -54,20 +57,20 @@ public class ZoneInitEventArgs : EventArgs var flags = *(byte*)(packet + 0x12); - eventArgs.TerritoryType = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x02)); + eventArgs.TerritoryType = LuminaUtils.CreateRef(*(ushort*)(packet + 0x02)); eventArgs.Instance = flags >= 0 ? (ushort)0 : *(ushort*)(packet + 0x04); - eventArgs.ContentFinderCondition = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x06)); - eventArgs.Weather = dataManager.GetExcelSheet().GetRow(*(byte*)(packet + 0x10)); + eventArgs.ContentFinderCondition = LuminaUtils.CreateRef(*(ushort*)(packet + 0x06)); + eventArgs.Weather = LuminaUtils.CreateRef(*(byte*)(packet + 0x10)); const int NumFestivals = 8; - eventArgs.ActiveFestivals = new Festival[NumFestivals]; + eventArgs.ActiveFestivals = new RowRef[NumFestivals]; eventArgs.ActiveFestivalPhases = new ushort[NumFestivals]; // There are also 4 festival ids and phases for PlayerState at +0x3E and +0x46 respectively, // but it's unclear why they exist as separate entries and why they would be different. for (var i = 0; i < NumFestivals; i++) { - eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x26 + (i * 2))); + eventArgs.ActiveFestivals[i] = LuminaUtils.CreateRef(*(ushort*)(packet + 0x26 + (i * 2))); eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2)); } From 10ef40ddf5651404470d9da0511fd55c5b4c4f1f Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:42:07 +0100 Subject: [PATCH 433/477] Update ClientStructs (#2595) 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 127047085..a02536a4b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 1270470855d6ac2d2f726b07019e21644c5658ec +Subproject commit a02536a4bf6862036403c03945a02fcd6689e445 From c0077b1e260a34e3e2f36ab8d4bd1b08aa9623d2 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 27 Jan 2026 09:30:34 -0800 Subject: [PATCH 434/477] Revert "Use RowRef in ZoneInitEventArgs (#2540)" (#2597) This reverts commit 5da79a7dbaa88a3c5695ec54b5882e19f393a5dc. --- Dalamud/Game/ClientState/ZoneInit.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/ClientState/ZoneInit.cs b/Dalamud/Game/ClientState/ZoneInit.cs index 7d6cda90f..7eb4576aa 100644 --- a/Dalamud/Game/ClientState/ZoneInit.cs +++ b/Dalamud/Game/ClientState/ZoneInit.cs @@ -3,11 +3,8 @@ using System.Text; using Dalamud.Data; -using Lumina.Excel; using Lumina.Excel.Sheets; -using Serilog; - namespace Dalamud.Game.ClientState; /// @@ -18,7 +15,7 @@ public class ZoneInitEventArgs : EventArgs /// /// Gets the territory type of the zone being entered. /// - public RowRef TerritoryType { get; private set; } + public TerritoryType TerritoryType { get; private set; } /// /// Gets the instance number of the zone, used when multiple copies of an area are active. @@ -28,17 +25,17 @@ public class ZoneInitEventArgs : EventArgs /// /// Gets the associated content finder condition for the zone, if any. /// - public RowRef ContentFinderCondition { get; private set; } + public ContentFinderCondition ContentFinderCondition { get; private set; } /// /// Gets the current weather in the zone upon entry. /// - public RowRef Weather { get; private set; } + public Weather Weather { get; private set; } /// /// Gets the set of active festivals in the zone. /// - public RowRef[] ActiveFestivals { get; private set; } = []; + public Festival[] ActiveFestivals { get; private set; } = []; /// /// Gets the phases corresponding to the active festivals. @@ -57,20 +54,20 @@ public class ZoneInitEventArgs : EventArgs var flags = *(byte*)(packet + 0x12); - eventArgs.TerritoryType = LuminaUtils.CreateRef(*(ushort*)(packet + 0x02)); + eventArgs.TerritoryType = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x02)); eventArgs.Instance = flags >= 0 ? (ushort)0 : *(ushort*)(packet + 0x04); - eventArgs.ContentFinderCondition = LuminaUtils.CreateRef(*(ushort*)(packet + 0x06)); - eventArgs.Weather = LuminaUtils.CreateRef(*(byte*)(packet + 0x10)); + eventArgs.ContentFinderCondition = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x06)); + eventArgs.Weather = dataManager.GetExcelSheet().GetRow(*(byte*)(packet + 0x10)); const int NumFestivals = 8; - eventArgs.ActiveFestivals = new RowRef[NumFestivals]; + eventArgs.ActiveFestivals = new Festival[NumFestivals]; eventArgs.ActiveFestivalPhases = new ushort[NumFestivals]; // There are also 4 festival ids and phases for PlayerState at +0x3E and +0x46 respectively, // but it's unclear why they exist as separate entries and why they would be different. for (var i = 0; i < NumFestivals; i++) { - eventArgs.ActiveFestivals[i] = LuminaUtils.CreateRef(*(ushort*)(packet + 0x26 + (i * 2))); + eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x26 + (i * 2))); eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2)); } From e598013e304b993a1ff079325cd62efb69292f23 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 27 Jan 2026 18:53:35 +0100 Subject: [PATCH 435/477] Upgrade goatcorp.Reloaded.Hooks, remove goatcorp.Reloaded.Assembler --- Dalamud/Dalamud.csproj | 1 - Directory.Packages.props | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 287bc5322..a1a08a908 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -65,7 +65,6 @@ - diff --git a/Directory.Packages.props b/Directory.Packages.props index 18760037b..2a8c52dc4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,7 @@ - - + From 5c7a5295d119c65510f9cc0b3fb4b8c4e418ea20 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:49:35 -0800 Subject: [PATCH 436/477] Misc Fixes (#2584) * Disable default logging, remove log message * Add IDtrBarEntry.MinimumWidth * Fix Addon/Agent Lifecycle Register/Unregister * Rename Agent.ReceiveEvent2 * Add to IReadOnlyDtrBarEntry * Fix autoformat being terrible * More style fixes * Add focused changed lifecycle event * Fix for obsolete renames --- .../AddonArgTypes/AddonFocusChangedArgs.cs | 22 ++++ Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 5 + Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 10 ++ .../Game/Addon/Lifecycle/AddonLifecycle.cs | 97 ++++++++++------ .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 34 ++++++ Dalamud/Game/Agent/AgentEvent.cs | 4 +- Dalamud/Game/Agent/AgentLifecycle.cs | 105 +++++++++++------- Dalamud/Game/Agent/AgentVirtualTable.cs | 20 ++-- Dalamud/Game/Gui/Dtr/DtrBar.cs | 10 +- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 29 +++++ 10 files changed, 245 insertions(+), 91 deletions(-) create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFocusChangedArgs.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFocusChangedArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFocusChangedArgs.cs new file mode 100644 index 000000000..8936a233b --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFocusChangedArgs.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for OnFocusChanged events. +/// +public class AddonFocusChangedArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonFocusChangedArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.FocusChanged; + + /// + /// Gets or sets a value indicating whether the window is being focused or unfocused. + /// + public bool ShouldFocus { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 46ee479ac..bc48eeed0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -44,4 +44,9 @@ public enum AddonArgsType /// Contains argument data for Close. /// Close, + + /// + /// Contains argument data for OnFocusChanged. + /// + FocusChanged, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 3b9c6e867..74c84d754 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -203,4 +203,14 @@ public enum AddonEvent /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows. /// PostFocus, + + /// + /// An event that is fired before an addon processes its FocusChanged method. + /// + PreFocusChanged, + + /// + /// An event that is fired after a addon processes its FocusChanged method. + /// + PostFocusChanged, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index c70c0c10f..6520ee4cf 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -31,7 +31,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private readonly Framework framework = Service.Get(); private Hook? onInitializeAddonHook; - private bool isInvokingListeners = false; + private bool isInvokingListeners; [ServiceManager.ServiceConstructor] private AddonLifecycle() @@ -56,29 +56,36 @@ internal unsafe class AddonLifecycle : IInternalDisposableService AllocatedTables.Clear(); } + /// + /// Resolves a virtual table address to the original virtual table address. + /// + /// The modified address to resolve. + /// The original address. + internal static AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress) + { + var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); + if (matchedTable == null) + { + return null; + } + + return matchedTable.OriginalVirtualTable; + } + /// /// Register a listener for the target event and addon. /// /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => + if (this.isInvokingListeners) { - if (!this.EventListeners.ContainsKey(listener.EventType)) - { - if (!this.EventListeners.TryAdd(listener.EventType, [])) - return; - } - - // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type - if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) - { - if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) - return; - } - - this.EventListeners[listener.EventType][listener.AddonName].Add(listener); - }, delayTicks: this.isInvokingListeners ? 1 : 0); + this.framework.RunOnTick(() => this.RegisterListenerMethod(listener)); + } + else + { + this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener)); + } } /// @@ -87,16 +94,14 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => + if (this.isInvokingListeners) { - if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) - { - if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) - { - addonListener.Remove(listener); - } - } - }, delayTicks: this.isInvokingListeners ? 1 : 0); + this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); + } + else + { + this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener)); + } } /// @@ -147,17 +152,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.isInvokingListeners = false; } - /// - /// Resolves a virtual table address to the original virtual table address. - /// - /// The modified address to resolve. - /// The original address. - internal AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress) + private void RegisterListenerMethod(AddonLifecycleEventListener listener) { - var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); - if (matchedTable == null) return null; + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + if (!this.EventListeners.TryAdd(listener.EventType, [])) + { + return; + } + } - return matchedTable.OriginalVirtualTable; + // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) + { + if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) + { + return; + } + } + + this.EventListeners[listener.EventType][listener.AddonName].Add(listener); + } + + private void UnregisterListenerMethod(AddonLifecycleEventListener listener) + { + if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) + { + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + { + addonListener.Remove(listener); + } + } } private void OnAddonInitialize(AtkUnitBase* addon) @@ -277,5 +302,5 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi /// public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) - => (nint)this.addonLifecycleService.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress); + => (nint)AddonLifecycle.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 736415738..1b2c828f8 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -42,6 +42,7 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonArgs onMouseOverArgs = new(); private readonly AddonArgs onMouseOutArgs = new(); private readonly AddonArgs focusArgs = new(); + private readonly AddonFocusChangedArgs focusChangedArgs = new(); private readonly AtkUnitBase* atkUnitBase; @@ -63,6 +64,7 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction; private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction; private readonly AtkUnitBase.Delegates.Focus focusFunction; + private readonly AtkUnitBase.Delegates.OnFocusChange onFocusChangeFunction; /// /// Initializes a new instance of the class. @@ -103,6 +105,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.onMouseOverFunction = this.OnAddonMouseOver; this.onMouseOutFunction = this.OnAddonMouseOut; this.focusFunction = this.OnAddonFocus; + this.onFocusChangeFunction = this.OnAddonFocusChange; // Overwrite specific virtual table entries this.ModifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); @@ -121,6 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); this.ModifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); + this.ModifiedVirtualTable->OnFocusChange = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onFocusChangeFunction); } /// @@ -630,6 +634,36 @@ internal unsafe class AddonVirtualTable : IDisposable } } + private void OnAddonFocusChange(AtkUnitBase* thisPtr, bool isFocused) + { + try + { + this.LogEvent(EnableLogging); + + this.focusChangedArgs.Addon = thisPtr; + this.focusChangedArgs.ShouldFocus = isFocused; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocusChanged, this.focusChangedArgs); + + isFocused = this.focusChangedArgs.ShouldFocus; + + try + { + this.OriginalVirtualTable->OnFocusChange(thisPtr, isFocused); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnFocusChanged. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocusChanged, this.focusChangedArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocusChange."); + } + } + [Conditional("DEBUG")] private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") { diff --git a/Dalamud/Game/Agent/AgentEvent.cs b/Dalamud/Game/Agent/AgentEvent.cs index 2a3002daa..e9c9a1b85 100644 --- a/Dalamud/Game/Agent/AgentEvent.cs +++ b/Dalamud/Game/Agent/AgentEvent.cs @@ -18,12 +18,12 @@ public enum AgentEvent /// /// An event that is fired before the agent processes its Filtered Receive Event Function. /// - PreReceiveFilteredEvent, + PreReceiveEventWithResult, /// /// An event that is fired after the agent has processed its Filtered Receive Event Function. /// - PostReceiveFilteredEvent, + PostReceiveEventWithResult, /// /// An event that is fired before the agent processes its Show Function. diff --git a/Dalamud/Game/Agent/AgentLifecycle.cs b/Dalamud/Game/Agent/AgentLifecycle.cs index 75ed47d86..45f0dec5c 100644 --- a/Dalamud/Game/Agent/AgentLifecycle.cs +++ b/Dalamud/Game/Agent/AgentLifecycle.cs @@ -69,30 +69,36 @@ internal unsafe class AgentLifecycle : IInternalDisposableService AllocatedTables.Clear(); } + /// + /// Resolves a virtual table address to the original virtual table address. + /// + /// The modified address to resolve. + /// The original address. + internal static AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress) + { + var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); + if (matchedTable == null) + { + return null; + } + + return matchedTable.OriginalVirtualTable; + } + /// /// Register a listener for the target event and agent. /// /// The listener to register. internal void RegisterListener(AgentLifecycleEventListener listener) { - this.framework.RunOnTick(() => + if (this.isInvokingListeners) { - if (!this.EventListeners.ContainsKey(listener.EventType)) - { - if (!this.EventListeners.TryAdd(listener.EventType, [])) - return; - } - - // Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type - if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId)) - { - if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, [])) - return; - } - - this.EventListeners[listener.EventType][listener.AgentId].Add(listener); - }, - delayTicks: this.isInvokingListeners ? 1 : 0); + this.framework.RunOnTick(() => this.RegisterListenerMethod(listener)); + } + else + { + this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener)); + } } /// @@ -101,17 +107,14 @@ internal unsafe class AgentLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AgentLifecycleEventListener listener) { - this.framework.RunOnTick(() => + if (this.isInvokingListeners) { - if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners)) - { - if (agentListeners.TryGetValue(listener.AgentId, out var agentListener)) - { - agentListener.Remove(listener); - } - } - }, - delayTicks: this.isInvokingListeners ? 1 : 0); + this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); + } + else + { + this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener)); + } } /// @@ -162,19 +165,6 @@ internal unsafe class AgentLifecycle : IInternalDisposableService this.isInvokingListeners = false; } - /// - /// Resolves a virtual table address to the original virtual table address. - /// - /// The modified address to resolve. - /// The original address. - internal AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress) - { - var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); - if (matchedTable == null) return null; - - return matchedTable.OriginalVirtualTable; - } - private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule) { this.onInitializeAgentsHook!.Original(thisPtr, uiModule); @@ -193,6 +183,39 @@ internal unsafe class AgentLifecycle : IInternalDisposableService } } + private void RegisterListenerMethod(AgentLifecycleEventListener listener) + { + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + if (!this.EventListeners.TryAdd(listener.EventType, [])) + { + return; + } + } + + // Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId)) + { + if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, [])) + { + return; + } + } + + this.EventListeners[listener.EventType][listener.AgentId].Add(listener); + } + + private void UnregisterListenerMethod(AgentLifecycleEventListener listener) + { + if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners)) + { + if (agentListeners.TryGetValue(listener.AgentId, out var agentListener)) + { + agentListener.Remove(listener); + } + } + } + private void ReplaceVirtualTables(AgentModule* agentModule) { foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length)) @@ -311,5 +334,5 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi /// public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) - => (nint)this.agentLifecycleService.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress); + => (nint)AgentLifecycle.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress); } diff --git a/Dalamud/Game/Agent/AgentVirtualTable.cs b/Dalamud/Game/Agent/AgentVirtualTable.cs index e7f9a2f6e..99f613137 100644 --- a/Dalamud/Game/Agent/AgentVirtualTable.cs +++ b/Dalamud/Game/Agent/AgentVirtualTable.cs @@ -21,7 +21,7 @@ internal unsafe class AgentVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 60; - private const bool EnableLogging = true; + private const bool EnableLogging = false; private static readonly ModuleLog Log = new("AgentVT"); @@ -44,7 +44,7 @@ internal unsafe class AgentVirtualTable : IDisposable // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table, // the CLR needs to know they are in use, or it will invalidate them causing random crashing. private readonly AgentInterface.Delegates.ReceiveEvent receiveEventFunction; - private readonly AgentInterface.Delegates.ReceiveEvent2 filteredReceiveEventFunction; + private readonly AgentInterface.Delegates.ReceiveEventWithResult receiveEventWithResultFunction; private readonly AgentInterface.Delegates.Show showFunction; private readonly AgentInterface.Delegates.Hide hideFunction; private readonly AgentInterface.Delegates.Update updateFunction; @@ -60,8 +60,6 @@ internal unsafe class AgentVirtualTable : IDisposable /// Reference to AgentLifecycle service to callback and invoke listeners. internal AgentVirtualTable(AgentInterface* agent, AgentId agentId, AgentLifecycle lifecycleService) { - Log.Debug($"Initializing AgentVirtualTable for {agentId}, Address: {(nint)agent:X}"); - this.agentInterface = agent; this.agentId = agentId; this.lifecycleService = lifecycleService; @@ -80,7 +78,7 @@ internal unsafe class AgentVirtualTable : IDisposable // Pin each of our listener functions this.receiveEventFunction = this.OnAgentReceiveEvent; - this.filteredReceiveEventFunction = this.OnAgentFilteredReceiveEvent; + this.receiveEventWithResultFunction = this.OnAgentReceiveEventWithResult; this.showFunction = this.OnAgentShow; this.hideFunction = this.OnAgentHide; this.updateFunction = this.OnAgentUpdate; @@ -90,7 +88,7 @@ internal unsafe class AgentVirtualTable : IDisposable // Overwrite specific virtual table entries this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction); - this.ModifiedVirtualTable->ReceiveEvent2 = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.filteredReceiveEventFunction); + this.ModifiedVirtualTable->ReceiveEventWithResult = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.receiveEventWithResultFunction); this.ModifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); this.ModifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); this.ModifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); @@ -158,7 +156,7 @@ internal unsafe class AgentVirtualTable : IDisposable return result; } - private AtkValue* OnAgentFilteredReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) + private AtkValue* OnAgentReceiveEventWithResult(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) { AtkValue* result = null; @@ -173,7 +171,7 @@ internal unsafe class AgentVirtualTable : IDisposable this.filteredReceiveEventArgs.ValueCount = valueCount; this.filteredReceiveEventArgs.EventKind = eventKind; - this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveFilteredEvent, this.filteredReceiveEventArgs); + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEventWithResult, this.filteredReceiveEventArgs); returnValue = (AtkValue*)this.filteredReceiveEventArgs.ReturnValue; values = (AtkValue*)this.filteredReceiveEventArgs.AtkValues; @@ -182,18 +180,18 @@ internal unsafe class AgentVirtualTable : IDisposable try { - result = this.OriginalVirtualTable->ReceiveEvent2(thisPtr, returnValue, values, valueCount, eventKind); + result = this.OriginalVirtualTable->ReceiveEventWithResult(thisPtr, returnValue, values, valueCount, eventKind); } catch (Exception e) { Log.Error(e, "Caught exception when calling original Agent FilteredReceiveEvent. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveFilteredEvent, this.filteredReceiveEventArgs); + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEventWithResult, this.filteredReceiveEventArgs); } catch (Exception e) { - Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentFilteredReceiveEvent."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEventWithResult."); } return result; diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 5663d0748..e5de6b2bd 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -397,7 +397,15 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar ushort w = 0, h = 0; node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); - node->SetWidth(w); + + if (data.MinimumWidth > 0) + { + node->SetWidth(Math.Max(data.MinimumWidth, w)); + } + else + { + node->SetWidth(w); + } } var elementWidth = data.TextNode->Width + this.configuration.DtrSpacing; diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index e0bd8fd49..47e86fde1 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -40,6 +40,11 @@ public interface IReadOnlyDtrBarEntry /// public bool Shown { get; } + /// + /// Gets a value indicating this entry's minimum width. + /// + public ushort MinimumWidth { get; } + /// /// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings. /// @@ -76,6 +81,11 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry /// public new bool Shown { get; set; } + /// + /// Gets or sets a value specifying the requested minimum width to make this entry. + /// + public new ushort MinimumWidth { get; set; } + /// /// Gets or sets an action to be invoked when the user clicks on the dtr entry. /// @@ -128,6 +138,25 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry /// public SeString? Tooltip { get; set; } + /// + public ushort MinimumWidth + { + get; + set + { + field = value; + if (this.TextNode is not null) + { + if (this.TextNode->GetWidth() < value) + { + this.TextNode->SetWidth(value); + } + } + + this.Dirty = true; + } + } + /// public Action? OnClick { get; set; } From 470267a18590157633022ec100b1cfd41cf5cd6c Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 27 Jan 2026 23:17:22 +0100 Subject: [PATCH 437/477] Restore NetworkMessageDirection enum Fixes API breakage --- .../Game/Network/NetworkMessageDirection.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Dalamud/Game/Network/NetworkMessageDirection.cs diff --git a/Dalamud/Game/Network/NetworkMessageDirection.cs b/Dalamud/Game/Network/NetworkMessageDirection.cs new file mode 100644 index 000000000..12cfc3d17 --- /dev/null +++ b/Dalamud/Game/Network/NetworkMessageDirection.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Network; + +/// +/// This represents the direction of a network message. +/// +[Obsolete("No longer part of public API", true)] +public enum NetworkMessageDirection +{ + /// + /// A zone down message. + /// + ZoneDown, + + /// + /// A zone up message. + /// + ZoneUp, +} From 5c250c17254ec89db2aee7ba34afd2925eca3923 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 27 Jan 2026 23:43:51 +0100 Subject: [PATCH 438/477] Make Framework.DelayTicks() deadlock-safe before the game has started --- Dalamud/Game/Framework.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 035745684..e40274043 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -121,9 +121,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework /// public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) { - if (this.frameworkDestroy.IsCancellationRequested) + if (this.frameworkDestroy.IsCancellationRequested) // Going away return Task.FromCanceled(this.frameworkDestroy.Token); - if (numTicks <= 0) + if (numTicks <= 0 || this.frameworkThreadTaskScheduler.BoundThread == null) // Nonsense or before first tick return Task.CompletedTask; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); From 2b51a2a54e20138c9e049b788ffd27b7d0c348be Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 28 Jan 2026 00:35:23 +0100 Subject: [PATCH 439/477] build: 14.0.2.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a1a08a908..34b546faf 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 14.0.1.0 + 14.0.2.0 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 934df7da8adbf199eab6ec5dfa7bd182ea97ea9b Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Thu, 29 Jan 2026 00:16:41 +0900 Subject: [PATCH 440/477] Properly lowercase for tags for plugin installer search --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index e32d31181..d132f0d8d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3804,7 +3804,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (!manifest.Punchline.IsNullOrEmpty()) scores.Add(matcher.Matches(manifest.Punchline.ToLowerInvariant()) * 100); if (manifest.Tags != null) - scores.Add(matcher.MatchesAny(manifest.Tags.ToArray()) * 100); + scores.Add(matcher.MatchesAny(manifest.Tags.Select(tag => tag.ToLowerInvariant()).ToArray()) * 100); return scores.Max(); } From 73edaadbcad5311c8cc7aa28e88d599a36c3b273 Mon Sep 17 00:00:00 2001 From: pohky <1510568+pohky@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:51:29 +0100 Subject: [PATCH 441/477] Fix null characters in BitmapCodecInfo strings --- Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs index ec56caadd..7f5ed4fda 100644 --- a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs +++ b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs @@ -50,6 +50,6 @@ internal sealed class BitmapCodecInfo : IBitmapCodecInfo _ = readFuncPtr(codecInfo, 0, null, &cch); var buf = stackalloc char[(int)cch + 1]; Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, buf, &cch)); - return new(buf, 0, (int)cch); + return new string(buf, 0, (int)cch).Trim('\0'); } } From 252b7eeb9b4d6b3081471b283538c4c1b3203126 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 30 Jan 2026 19:21:46 +0100 Subject: [PATCH 442/477] Replace PeHeader with TerraFX --- Dalamud/Hooking/Hook.cs | 30 ++- Dalamud/Hooking/Internal/PeHeader.cs | 390 --------------------------- 2 files changed, 17 insertions(+), 403 deletions(-) delete mode 100644 Dalamud/Hooking/Internal/PeHeader.cs diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index c54a8f399..265b118bc 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -6,6 +6,8 @@ using Dalamud.Configuration.Internal; using Dalamud.Hooking.Internal; using Dalamud.Hooking.Internal.Verification; +using TerraFX.Interop.Windows; + namespace Dalamud.Hooking; /// @@ -20,6 +22,8 @@ public abstract class Hook : IDalamudHook where T : Delegate private const ulong IMAGE_ORDINAL_FLAG64 = 0x8000000000000000; // ReSharper disable once InconsistentNaming private const uint IMAGE_ORDINAL_FLAG32 = 0x80000000; + // ReSharper disable once InconsistentNaming + private const int IMAGE_DIRECTORY_ENTRY_IMPORT = 1; #pragma warning restore SA1310 private readonly IntPtr address; @@ -124,25 +128,25 @@ public abstract class Hook : IDalamudHook where T : Delegate module ??= Process.GetCurrentProcess().MainModule; if (module == null) throw new InvalidOperationException("Current module is null?"); - var pDos = (PeHeader.IMAGE_DOS_HEADER*)module.BaseAddress; - var pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4); - var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf(); - PeHeader.IMAGE_DATA_DIRECTORY* pDataDirectory; + var pDos = (IMAGE_DOS_HEADER*)module.BaseAddress; + var pNt = (IMAGE_FILE_HEADER*)(module.BaseAddress + pDos->e_lfanew + 4); + var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf(); + IMAGE_DATA_DIRECTORY* pDataDirectory; if (isPe64) { - var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf()); - pDataDirectory = &pOpt->ImportTable; + var pOpt = (IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf()); + pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; } else { - var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf()); - pDataDirectory = &pOpt->ImportTable; + var pOpt = (IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf()); + pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; } var moduleNameLowerWithNullTerminator = (moduleName + "\0").ToLowerInvariant(); - foreach (ref var importDescriptor in new Span( - (PeHeader.IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress), - (int)(pDataDirectory->Size / Marshal.SizeOf()))) + foreach (ref var importDescriptor in new Span( + (IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress), + (int)(pDataDirectory->Size / Marshal.SizeOf()))) { // Having all zero values signals the end of the table. We didn't find anything. if (importDescriptor.Characteristics == 0) @@ -248,7 +252,7 @@ public abstract class Hook : IDalamudHook where T : Delegate ObjectDisposedException.ThrowIf(this.IsDisposed, this); } - private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) + private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref IMAGE_IMPORT_DESCRIPTOR desc, ref IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) { var importLookupsOversizedSpan = new Span((uint*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf())); var importAddressesOversizedSpan = new Span((uint*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf())); @@ -298,7 +302,7 @@ public abstract class Hook : IDalamudHook where T : Delegate throw new MissingMethodException("Specified method not found"); } - private static unsafe IntPtr FromImportHelper64(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) + private static unsafe IntPtr FromImportHelper64(IntPtr baseAddress, ref IMAGE_IMPORT_DESCRIPTOR desc, ref IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) { var importLookupsOversizedSpan = new Span((ulong*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf())); var importAddressesOversizedSpan = new Span((ulong*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf())); diff --git a/Dalamud/Hooking/Internal/PeHeader.cs b/Dalamud/Hooking/Internal/PeHeader.cs deleted file mode 100644 index 51df4a174..000000000 --- a/Dalamud/Hooking/Internal/PeHeader.cs +++ /dev/null @@ -1,390 +0,0 @@ -using System.Runtime.InteropServices; - -#pragma warning disable -namespace Dalamud.Hooking.Internal; - -internal class PeHeader -{ - public struct IMAGE_DOS_HEADER - { - public UInt16 e_magic; - public UInt16 e_cblp; - public UInt16 e_cp; - public UInt16 e_crlc; - public UInt16 e_cparhdr; - public UInt16 e_minalloc; - public UInt16 e_maxalloc; - public UInt16 e_ss; - public UInt16 e_sp; - public UInt16 e_csum; - public UInt16 e_ip; - public UInt16 e_cs; - public UInt16 e_lfarlc; - public UInt16 e_ovno; - public UInt16 e_res_0; - public UInt16 e_res_1; - public UInt16 e_res_2; - public UInt16 e_res_3; - public UInt16 e_oemid; - public UInt16 e_oeminfo; - public UInt16 e_res2_0; - public UInt16 e_res2_1; - public UInt16 e_res2_2; - public UInt16 e_res2_3; - public UInt16 e_res2_4; - public UInt16 e_res2_5; - public UInt16 e_res2_6; - public UInt16 e_res2_7; - public UInt16 e_res2_8; - public UInt16 e_res2_9; - public UInt32 e_lfanew; - } - - [StructLayout(LayoutKind.Sequential)] - public struct IMAGE_DATA_DIRECTORY - { - public UInt32 VirtualAddress; - public UInt32 Size; - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct IMAGE_OPTIONAL_HEADER32 - { - public UInt16 Magic; - public Byte MajorLinkerVersion; - public Byte MinorLinkerVersion; - public UInt32 SizeOfCode; - public UInt32 SizeOfInitializedData; - public UInt32 SizeOfUninitializedData; - public UInt32 AddressOfEntryPoint; - public UInt32 BaseOfCode; - public UInt32 BaseOfData; - public UInt32 ImageBase; - public UInt32 SectionAlignment; - public UInt32 FileAlignment; - public UInt16 MajorOperatingSystemVersion; - public UInt16 MinorOperatingSystemVersion; - public UInt16 MajorImageVersion; - public UInt16 MinorImageVersion; - public UInt16 MajorSubsystemVersion; - public UInt16 MinorSubsystemVersion; - public UInt32 Win32VersionValue; - public UInt32 SizeOfImage; - public UInt32 SizeOfHeaders; - public UInt32 CheckSum; - public UInt16 Subsystem; - public UInt16 DllCharacteristics; - public UInt32 SizeOfStackReserve; - public UInt32 SizeOfStackCommit; - public UInt32 SizeOfHeapReserve; - public UInt32 SizeOfHeapCommit; - public UInt32 LoaderFlags; - public UInt32 NumberOfRvaAndSizes; - - public IMAGE_DATA_DIRECTORY ExportTable; - public IMAGE_DATA_DIRECTORY ImportTable; - public IMAGE_DATA_DIRECTORY ResourceTable; - public IMAGE_DATA_DIRECTORY ExceptionTable; - public IMAGE_DATA_DIRECTORY CertificateTable; - public IMAGE_DATA_DIRECTORY BaseRelocationTable; - public IMAGE_DATA_DIRECTORY Debug; - public IMAGE_DATA_DIRECTORY Architecture; - public IMAGE_DATA_DIRECTORY GlobalPtr; - public IMAGE_DATA_DIRECTORY TLSTable; - public IMAGE_DATA_DIRECTORY LoadConfigTable; - public IMAGE_DATA_DIRECTORY BoundImport; - public IMAGE_DATA_DIRECTORY IAT; - public IMAGE_DATA_DIRECTORY DelayImportDescriptor; - public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; - public IMAGE_DATA_DIRECTORY Reserved; - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct IMAGE_OPTIONAL_HEADER64 - { - public UInt16 Magic; - public Byte MajorLinkerVersion; - public Byte MinorLinkerVersion; - public UInt32 SizeOfCode; - public UInt32 SizeOfInitializedData; - public UInt32 SizeOfUninitializedData; - public UInt32 AddressOfEntryPoint; - public UInt32 BaseOfCode; - public UInt64 ImageBase; - public UInt32 SectionAlignment; - public UInt32 FileAlignment; - public UInt16 MajorOperatingSystemVersion; - public UInt16 MinorOperatingSystemVersion; - public UInt16 MajorImageVersion; - public UInt16 MinorImageVersion; - public UInt16 MajorSubsystemVersion; - public UInt16 MinorSubsystemVersion; - public UInt32 Win32VersionValue; - public UInt32 SizeOfImage; - public UInt32 SizeOfHeaders; - public UInt32 CheckSum; - public UInt16 Subsystem; - public UInt16 DllCharacteristics; - public UInt64 SizeOfStackReserve; - public UInt64 SizeOfStackCommit; - public UInt64 SizeOfHeapReserve; - public UInt64 SizeOfHeapCommit; - public UInt32 LoaderFlags; - public UInt32 NumberOfRvaAndSizes; - - public IMAGE_DATA_DIRECTORY ExportTable; - public IMAGE_DATA_DIRECTORY ImportTable; - public IMAGE_DATA_DIRECTORY ResourceTable; - public IMAGE_DATA_DIRECTORY ExceptionTable; - public IMAGE_DATA_DIRECTORY CertificateTable; - public IMAGE_DATA_DIRECTORY BaseRelocationTable; - public IMAGE_DATA_DIRECTORY Debug; - public IMAGE_DATA_DIRECTORY Architecture; - public IMAGE_DATA_DIRECTORY GlobalPtr; - public IMAGE_DATA_DIRECTORY TLSTable; - public IMAGE_DATA_DIRECTORY LoadConfigTable; - public IMAGE_DATA_DIRECTORY BoundImport; - public IMAGE_DATA_DIRECTORY IAT; - public IMAGE_DATA_DIRECTORY DelayImportDescriptor; - public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; - public IMAGE_DATA_DIRECTORY Reserved; - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct IMAGE_FILE_HEADER - { - public UInt16 Machine; - public UInt16 NumberOfSections; - public UInt32 TimeDateStamp; - public UInt32 PointerToSymbolTable; - public UInt32 NumberOfSymbols; - public UInt16 SizeOfOptionalHeader; - public UInt16 Characteristics; - } - - [StructLayout(LayoutKind.Explicit)] - public struct IMAGE_SECTION_HEADER - { - [FieldOffset(0)] - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] - public char[] Name; - [FieldOffset(8)] - public UInt32 VirtualSize; - [FieldOffset(12)] - public UInt32 VirtualAddress; - [FieldOffset(16)] - public UInt32 SizeOfRawData; - [FieldOffset(20)] - public UInt32 PointerToRawData; - [FieldOffset(24)] - public UInt32 PointerToRelocations; - [FieldOffset(28)] - public UInt32 PointerToLinenumbers; - [FieldOffset(32)] - public UInt16 NumberOfRelocations; - [FieldOffset(34)] - public UInt16 NumberOfLinenumbers; - [FieldOffset(36)] - public DataSectionFlags Characteristics; - - public string Section - { - get { return new string(Name); } - } - } - - [Flags] - public enum DataSectionFlags : uint - { - /// - /// Reserved for future use. - /// - TypeReg = 0x00000000, - /// - /// Reserved for future use. - /// - TypeDsect = 0x00000001, - /// - /// Reserved for future use. - /// - TypeNoLoad = 0x00000002, - /// - /// Reserved for future use. - /// - TypeGroup = 0x00000004, - /// - /// The section should not be padded to the next boundary. This flag is obsolete and is replaced by IMAGE_SCN_ALIGN_1BYTES. This is valid only for object files. - /// - TypeNoPadded = 0x00000008, - /// - /// Reserved for future use. - /// - TypeCopy = 0x00000010, - /// - /// The section contains executable code. - /// - ContentCode = 0x00000020, - /// - /// The section contains initialized data. - /// - ContentInitializedData = 0x00000040, - /// - /// The section contains uninitialized data. - /// - ContentUninitializedData = 0x00000080, - /// - /// Reserved for future use. - /// - LinkOther = 0x00000100, - /// - /// The section contains comments or other information. The .drectve section has this type. This is valid for object files only. - /// - LinkInfo = 0x00000200, - /// - /// Reserved for future use. - /// - TypeOver = 0x00000400, - /// - /// The section will not become part of the image. This is valid only for object files. - /// - LinkRemove = 0x00000800, - /// - /// The section contains COMDAT data. For more information, see section 5.5.6, COMDAT Sections (Object Only). This is valid only for object files. - /// - LinkComDat = 0x00001000, - /// - /// Reset speculative exceptions handling bits in the TLB entries for this section. - /// - NoDeferSpecExceptions = 0x00004000, - /// - /// The section contains data referenced through the global pointer (GP). - /// - RelativeGP = 0x00008000, - /// - /// Reserved for future use. - /// - MemPurgeable = 0x00020000, - /// - /// Reserved for future use. - /// - Memory16Bit = 0x00020000, - /// - /// Reserved for future use. - /// - MemoryLocked = 0x00040000, - /// - /// Reserved for future use. - /// - MemoryPreload = 0x00080000, - /// - /// Align data on a 1-byte boundary. Valid only for object files. - /// - Align1Bytes = 0x00100000, - /// - /// Align data on a 2-byte boundary. Valid only for object files. - /// - Align2Bytes = 0x00200000, - /// - /// Align data on a 4-byte boundary. Valid only for object files. - /// - Align4Bytes = 0x00300000, - /// - /// Align data on an 8-byte boundary. Valid only for object files. - /// - Align8Bytes = 0x00400000, - /// - /// Align data on a 16-byte boundary. Valid only for object files. - /// - Align16Bytes = 0x00500000, - /// - /// Align data on a 32-byte boundary. Valid only for object files. - /// - Align32Bytes = 0x00600000, - /// - /// Align data on a 64-byte boundary. Valid only for object files. - /// - Align64Bytes = 0x00700000, - /// - /// Align data on a 128-byte boundary. Valid only for object files. - /// - Align128Bytes = 0x00800000, - /// - /// Align data on a 256-byte boundary. Valid only for object files. - /// - Align256Bytes = 0x00900000, - /// - /// Align data on a 512-byte boundary. Valid only for object files. - /// - Align512Bytes = 0x00A00000, - /// - /// Align data on a 1024-byte boundary. Valid only for object files. - /// - Align1024Bytes = 0x00B00000, - /// - /// Align data on a 2048-byte boundary. Valid only for object files. - /// - Align2048Bytes = 0x00C00000, - /// - /// Align data on a 4096-byte boundary. Valid only for object files. - /// - Align4096Bytes = 0x00D00000, - /// - /// Align data on an 8192-byte boundary. Valid only for object files. - /// - Align8192Bytes = 0x00E00000, - /// - /// The section contains extended relocations. - /// - LinkExtendedRelocationOverflow = 0x01000000, - /// - /// The section can be discarded as needed. - /// - MemoryDiscardable = 0x02000000, - /// - /// The section cannot be cached. - /// - MemoryNotCached = 0x04000000, - /// - /// The section is not pageable. - /// - MemoryNotPaged = 0x08000000, - /// - /// The section can be shared in memory. - /// - MemoryShared = 0x10000000, - /// - /// The section can be executed as code. - /// - MemoryExecute = 0x20000000, - /// - /// The section can be read. - /// - MemoryRead = 0x40000000, - /// - /// The section can be written to. - /// - MemoryWrite = 0x80000000 - } - - [StructLayout(LayoutKind.Explicit)] - public struct IMAGE_IMPORT_DESCRIPTOR - { - [FieldOffset(0)] - public uint Characteristics; - - [FieldOffset(0)] - public uint OriginalFirstThunk; - - [FieldOffset(4)] - public uint TimeDateStamp; - - [FieldOffset(8)] - public uint ForwarderChain; - - [FieldOffset(12)] - public uint Name; - - [FieldOffset(16)] - public uint FirstThunk; - } -} From aa4ace976e12e3dcbdd0f9f424900b27047a690d Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:56:36 +0100 Subject: [PATCH 443/477] Update ClientStructs (#2598) 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 a02536a4b..cb1f076a6 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit a02536a4bf6862036403c03945a02fcd6689e445 +Subproject commit cb1f076a6fcb6131cd8e1d5bda438adc980b3ee3 From 33a7cdefa8b47ef0ec7d25f62971aed613979ca1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 31 Jan 2026 21:57:11 +0100 Subject: [PATCH 444/477] Switch to CS in NetworkMonitorWidget (#2600) * Switch to CS in NetworkMonitorWidget * Lazy-init the hooks * Fix warnings --- Dalamud/Game/Gui/ContextMenu/ContextMenu.cs | 2 +- .../Windows/Data/Widgets/GaugeWidget.cs | 2 +- .../Windows/Data/Widgets/HookWidget.cs | 2 +- .../Data/Widgets/NetworkMonitorWidget.cs | 28 ++++++++----------- Dalamud/Plugin/Services/ITextureProvider.cs | 4 +-- 5 files changed, 16 insertions(+), 22 deletions(-) diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs index 0b306f093..ab3da7bf3 100644 --- a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -336,7 +336,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM this.MenuCallbackIds.Clear(); this.SelectedAgent = agent; var unitManager = RaptureAtkUnitManager.Instance(); - this.SelectedParentAddon = unitManager->GetAddonById(unitManager->GetAddonByName(addonName)->ContextMenuParentId); + this.SelectedParentAddon = unitManager->GetAddonById(unitManager->GetAddonByName(addonName)->BlockedParentId); this.SelectedEventInterfaces.Clear(); if (this.SelectedAgent == AgentInventoryContext.Instance()) { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs index 74403a32b..8badcfaf8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs @@ -62,7 +62,7 @@ internal class GaugeWidget : IDataWindowWidget 40 => jobGauges.Get(), 41 => jobGauges.Get(), 42 => jobGauges.Get(), - _ => null + _ => null, }; if (gauge == null) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index fd27996ed..e19281b40 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -50,7 +50,7 @@ internal unsafe class HookWidget : IDataWindowWidget { MessageBoxW, AddonFinalize, - Random + Random, } /// diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 922b72717..4460a9f9a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Threading; using Dalamud.Bindings.ImGui; -using Dalamud.Game; using Dalamud.Hooking; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; @@ -23,7 +22,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget private readonly ConcurrentQueue packets = new(); private Hook? hookDown; - private Hook? hookUp; + private Hook? hookUp; private bool trackNetwork; private int trackedPackets = 20; @@ -40,8 +39,6 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget this.hookUp?.Dispose(); } - private delegate byte ZoneClientSendPacketDelegate(ZoneClient* thisPtr, nint packet, uint a3, uint a4, byte a5); - private enum NetworkMessageDirection { ZoneDown, @@ -58,22 +55,19 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget public bool Ready { get; set; } /// - public void Load() - { - this.hookDown = Hook.FromAddress( - (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket, - this.OnReceivePacketDetour); - - // TODO: switch to ZoneClient.SendPacket from CS - if (Service.Get().TryScanText("E8 ?? ?? ?? ?? 4C 8B 44 24 ?? E9", out var address)) - this.hookUp = Hook.FromAddress(address, this.SendPacketDetour); - - this.Ready = true; - } + public void Load() => this.Ready = true; /// public void Draw() { + this.hookDown ??= Hook.FromAddress( + (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket, + this.OnReceivePacketDetour); + + this.hookUp ??= Hook.FromAddress( + (nint)ZoneClient.MemberFunctionPointers.SendPacket, + this.SendPacketDetour); + if (ImGui.Checkbox("Track Network Packets"u8, ref this.trackNetwork)) { if (this.trackNetwork) @@ -213,7 +207,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget this.hookDown.OriginalDisposeSafe(thisPtr, targetId, packet); } - private byte SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, byte a5) + private bool SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, bool a5) { var opCode = *(ushort*)packet; this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty)); diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index a4d1dcbd2..63a463613 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; @@ -322,7 +322,7 @@ public interface ITextureProvider : IDalamudService /// Whether to leave non-disposed when the returned /// completes. /// Address of the new . - /// See PrintTextureInfo in for an example + /// See PrintTextureInfo in for an example /// of replacing the texture of an image node. /// /// If the returned kernel texture is to be destroyed, call the fourth function in its vtable, by calling From dc783e0c2b3e7a89e0ff05fe9bb61a343a9ccea3 Mon Sep 17 00:00:00 2001 From: Critical Impact Date: Mon, 2 Feb 2026 16:56:31 +1000 Subject: [PATCH 445/477] Use plugin internal name for datashare tracking --- .../Windows/Data/Widgets/DataShareWidget.cs | 4 +- Dalamud/Plugin/DalamudPluginInterface.cs | 8 ++-- Dalamud/Plugin/Ipc/Internal/DataShare.cs | 38 ++++++------------- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 73e9d18f8..54bb84ceb 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -87,7 +87,7 @@ internal class DataShareWidget : IDataWindowWidget try { var dataShare = Service.Get(); - var data2 = dataShare.GetData(name); + var data2 = dataShare.GetData(name, "DataShareWidget"); try { data = Encoding.UTF8.GetBytes( @@ -98,7 +98,7 @@ internal class DataShareWidget : IDataWindowWidget } finally { - dataShare.RelinquishData(name); + dataShare.RelinquishData(name, "DataShareWidget"); } } catch (Exception e) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index df1d0f6e9..28affddb4 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -227,19 +227,19 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// public T GetOrCreateData(string tag, Func dataGenerator) where T : class - => Service.Get().GetOrCreateData(tag, dataGenerator); + => Service.Get().GetOrCreateData(tag, this.plugin.InternalName, dataGenerator); /// public void RelinquishData(string tag) - => Service.Get().RelinquishData(tag); + => Service.Get().RelinquishData(tag, this.plugin.InternalName); /// public bool TryGetData(string tag, [NotNullWhen(true)] out T? data) where T : class - => Service.Get().TryGetData(tag, out data); + => Service.Get().TryGetData(tag, this.plugin.InternalName, out data); /// public T? GetData(string tag) where T : class - => Service.Get().GetData(tag); + => Service.Get().GetData(tag, this.plugin.InternalName); /// public ICallGateProvider GetIpcProvider(string name) diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index becbe1211..f71d8d2c2 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -33,16 +33,15 @@ internal class DataShare : IServiceType /// /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The name for the data cache. + /// The name of the caller. /// The function that generates the data if it does not already exist. /// Either the existing data for or the data generated by . /// Thrown if a cache for exists, but contains data of a type not assignable to . /// Thrown if the stored data for a cache is null. /// Thrown if throws an exception or returns null. - public T GetOrCreateData(string tag, Func dataGenerator) + public T GetOrCreateData(string tag, string callerName, Func dataGenerator) where T : class { - var callerName = GetCallerName(); - Lazy cacheLazy; lock (this.caches) { @@ -58,7 +57,8 @@ internal class DataShare : IServiceType /// If no assembly uses the data anymore, the cache will be removed from the data share and if it is an IDisposable, Dispose will be called on it. /// /// The name for the data cache. - public void RelinquishData(string tag) + /// The name of the caller. + public void RelinquishData(string tag, string callerName) { DataCache cache; lock (this.caches) @@ -66,8 +66,6 @@ internal class DataShare : IServiceType if (!this.caches.TryGetValue(tag, out var cacheLazy)) return; - var callerName = GetCallerName(); - cache = cacheLazy.Value; if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) return; @@ -84,7 +82,7 @@ internal class DataShare : IServiceType } catch (Exception e) { - Log.Error(e, "[DataShare] Failed to dispose [{Tag:l}] after it was removed from all shares.", tag); + Log.Error(e, "[DataShare] Failed to dispose [{Tag:l}] after it was removed from all shares.", tag); } } else @@ -99,9 +97,10 @@ internal class DataShare : IServiceType /// /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The name for the data cache. + /// The name of the caller. /// The requested data on success, null otherwise. /// True if the requested data exists and is assignable to the requested type. - public bool TryGetData(string tag, [NotNullWhen(true)] out T? data) + public bool TryGetData(string tag, string callerName, [NotNullWhen(true)] out T? data) where T : class { data = null; @@ -112,7 +111,7 @@ internal class DataShare : IServiceType return false; } - return cacheLazy.Value.TryGetData(GetCallerName(), out data, out _); + return cacheLazy.Value.TryGetData(callerName, out data, out _); } /// @@ -121,11 +120,12 @@ internal class DataShare : IServiceType /// /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The name for the data cache. + /// The name of the caller. /// The requested data. /// Thrown if is not registered. /// Thrown if a cache for exists, but contains data of a type not assignable to . /// Thrown if the stored data for a cache is null. - public T GetData(string tag) + public T GetData(string tag, string callerName) where T : class { Lazy cacheLazy; @@ -135,7 +135,7 @@ internal class DataShare : IServiceType throw new KeyNotFoundException($"The data cache [{tag}] is not registered."); } - return cacheLazy.Value.TryGetData(GetCallerName(), out var value, out var ex) ? value : throw ex; + return cacheLazy.Value.TryGetData(callerName, out var value, out var ex) ? value : throw ex; } /// @@ -150,20 +150,4 @@ internal class DataShare : IServiceType kvp => (kvp.Key, kvp.Value.Value.CreatorAssemblyName, kvp.Value.Value.UserAssemblyNames.ToArray())); } } - - /// Obtain the last assembly name in the stack trace that is not a system or dalamud assembly. - private static string GetCallerName() - { - var frames = new StackTrace().GetFrames(); - foreach (var frame in frames.Reverse()) - { - var name = frame.GetMethod()?.DeclaringType?.Assembly.GetName().Name ?? "Unknown"; - if (!name.StartsWith("System") && !name.StartsWith("Dalamud")) - { - return name; - } - } - - return "Unknown"; - } } From 9e18b843dba53995af1ca035c82e13f1545e67ae Mon Sep 17 00:00:00 2001 From: Critical Impact Date: Mon, 2 Feb 2026 22:32:33 +1000 Subject: [PATCH 446/477] Change column name --- .../Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 54bb84ceb..827d609a7 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -284,7 +284,7 @@ internal class DataShareWidget : IDataWindowWidget ImGui.TableSetupColumn("Shared Tag"u8); ImGui.TableSetupColumn("Show"u8); - ImGui.TableSetupColumn("Creator Assembly"u8); + ImGui.TableSetupColumn("Creator Internal Name"u8); ImGui.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("Consumers"u8); ImGui.TableHeadersRow(); From d8a13a72aa941bb41ff52020f1a848e197d4a65a Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 5 Feb 2026 00:20:39 +0100 Subject: [PATCH 447/477] - Add the CustomizeData struct to ICharacter - API 15 note the Customize array --- .../ClientState/Customize/CustomizeData.cs | 314 ++++++++++++++++++ .../ClientState/Objects/Types/Character.cs | 49 +-- 2 files changed, 344 insertions(+), 19 deletions(-) create mode 100644 Dalamud/Game/ClientState/Customize/CustomizeData.cs diff --git a/Dalamud/Game/ClientState/Customize/CustomizeData.cs b/Dalamud/Game/ClientState/Customize/CustomizeData.cs new file mode 100644 index 000000000..644bec5e8 --- /dev/null +++ b/Dalamud/Game/ClientState/Customize/CustomizeData.cs @@ -0,0 +1,314 @@ +using Dalamud.Game.ClientState.Objects.Types; + +namespace Dalamud.Game.ClientState.Customize; + +/// +/// This collection represents customization data a has. +/// +public interface ICustomizeData +{ + /// + /// Gets the current race. + /// E.g., Miqo'te, Aura + /// + public byte Race { get; } + + /// + /// Gets the current sex. + /// + public byte Sex { get; } + + /// + /// Gets the current body type. + /// + public byte BodyType { get; } + + /// + /// Gets the current height. + /// 0 to 100 + /// + public byte Height { get; } + + /// + /// Gets the current tribe. + /// E.g., Seeker of the Sun, Keeper of the Moon + /// + public byte Tribe { get; } + + /// + /// Gets the current face. + /// 1 to 4 + /// + public byte Face { get; } + + /// + /// Gets the current hairstyle. + /// + public byte Hairstyle { get; } + + /// + /// Gets the current skin color. + /// + public byte SkinColor { get; } + + /// + /// Gets the current color of the left eye. + /// + public byte EyeColorLeft { get; } + + /// + /// Gets the current color of the right eye. + /// + public byte EyeColorRight { get; } + + /// + /// Gets the current main hair color. + /// + public byte HairColor { get; } + + /// + /// Gets the current highlight hair color. + /// + public byte HighlightsColor { get; } + + /// + /// Gets the current tattoo color. + /// + public byte TattooColor { get; } + + /// + /// Gets the current eyebrow type. + /// + public byte Eyebrows { get; } + + /// + /// Gets the current nose type. + /// + public byte Nose { get; } + + /// + /// Gets the current jaw type. + /// + public byte Jaw { get; } + + /// + /// Gets the current lip color fur pattern. + /// + public byte LipColorFurPattern { get; } + + /// + /// Gets the current muscle mass value. + /// + public byte MuscleMass { get; } + + /// + /// Gets the current tail type. + /// + public byte TailShape { get; } + + /// + /// Gets the current bust size. + /// 0 to 100 + /// + public byte BustSize { get; } + + /// + /// Gets the current color of the face paint. + /// + public byte FacePaintColor { get; } + + /// + /// Gets a value indicating whether highlight color is used. + /// + public bool Highlights { get; } + + /// + /// Gets a value indicating whether this facial feature is used. + /// + public bool FacialFeature1 { get; } + + /// + public bool FacialFeature2 { get; } + + /// + public bool FacialFeature3 { get; } + + /// + public bool FacialFeature4 { get; } + + /// + public bool FacialFeature5 { get; } + + /// + public bool FacialFeature6 { get; } + + /// + public bool FacialFeature7 { get; } + + /// + /// Gets a value indicating whether the legacy tattoo is used. + /// + public bool LegacyTattoo { get; } + + /// + /// Gets the current eye shape type. + /// + public byte EyeShape { get; } + + /// + /// Gets a value indicating whether small iris is used. + /// + public bool SmallIris { get; } + + /// + /// Gets the current mouth type. + /// + public byte Mouth { get; } + + /// + /// Gets a value indicating whether lipstick is used. + /// + public bool Lipstick { get; } + + /// + /// Gets the current face paint type. + /// + public byte FacePaint { get; } + + /// + /// Gets a value indicating whether face paint reversed is used. + /// + public bool FacePaintReversed { get; } +} + +/// +internal readonly unsafe struct CustomizeData : ICustomizeData +{ + /// + /// Gets or sets the address of the customize data struct in memory. + /// + public readonly nint Address; + + /// + /// Initializes a new instance of the struct. + /// + /// Address of the status list. + internal CustomizeData(nint address) + { + this.Address = address; + } + + /// + public byte Race => this.Struct->Race; + + /// + public byte Sex => this.Struct->Sex; + + /// + public byte BodyType => this.Struct->BodyType; + + /// + public byte Height => this.Struct->Height; + + /// + public byte Tribe => this.Struct->Tribe; + + /// + public byte Face => this.Struct->Face; + + /// + public byte Hairstyle => this.Struct->Hairstyle; + + /// + public byte SkinColor => this.Struct->SkinColor; + + /// + public byte EyeColorLeft => this.Struct->EyeColorLeft; + + /// + public byte EyeColorRight => this.Struct->EyeColorRight; + + /// + public byte HairColor => this.Struct->HairColor; + + /// + public byte HighlightsColor => this.Struct->HighlightsColor; + + /// + public byte TattooColor => this.Struct->TattooColor; + + /// + public byte Eyebrows => this.Struct->Eyebrows; + + /// + public byte Nose => this.Struct->Nose; + + /// + public byte Jaw => this.Struct->Jaw; + + /// + public byte LipColorFurPattern => this.Struct->LipColorFurPattern; + + /// + public byte MuscleMass => this.Struct->MuscleMass; + + /// + public byte TailShape => this.Struct->TailShape; + + /// + public byte BustSize => this.Struct->BustSize; + + /// + public byte FacePaintColor => this.Struct->FacePaintColor; + + /// + public bool Highlights => this.Struct->Highlights; + + /// + public bool FacialFeature1 => this.Struct->FacialFeature1; + + /// + public bool FacialFeature2 => this.Struct->FacialFeature2; + + /// + public bool FacialFeature3 => this.Struct->FacialFeature3; + + /// + public bool FacialFeature4 => this.Struct->FacialFeature4; + + /// + public bool FacialFeature5 => this.Struct->FacialFeature5; + + /// + public bool FacialFeature6 => this.Struct->FacialFeature6; + + /// + public bool FacialFeature7 => this.Struct->FacialFeature7; + + /// + public bool LegacyTattoo => this.Struct->LegacyTattoo; + + /// + public byte EyeShape => this.Struct->EyeShape; + + /// + public bool SmallIris => this.Struct->SmallIris; + + /// + public byte Mouth => this.Struct->Mouth; + + /// + public bool Lipstick => this.Struct->Lipstick; + + /// + public byte FacePaint => this.Struct->FacePaint; + + /// + public bool FacePaintReversed => this.Struct->FacePaintReversed; + + /// + /// Gets the underlying structure. + /// + internal FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData* Struct => + (FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData*)this.Address; +} diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index 2002a16b8..f122f1f27 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -1,6 +1,8 @@ using Dalamud.Data; +using Dalamud.Game.ClientState.Customize; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Utility; using Lumina.Excel; using Lumina.Excel.Sheets; @@ -13,68 +15,73 @@ namespace Dalamud.Game.ClientState.Objects.Types; public interface ICharacter : IGameObject { /// - /// Gets the current HP of this Chara. + /// Gets the current HP of this character. /// public uint CurrentHp { get; } /// - /// Gets the maximum HP of this Chara. + /// Gets the maximum HP of this character. /// public uint MaxHp { get; } /// - /// Gets the current MP of this Chara. + /// Gets the current MP of this character. /// public uint CurrentMp { get; } /// - /// Gets the maximum MP of this Chara. + /// Gets the maximum MP of this character. /// public uint MaxMp { get; } /// - /// Gets the current GP of this Chara. + /// Gets the current GP of this character. /// public uint CurrentGp { get; } /// - /// Gets the maximum GP of this Chara. + /// Gets the maximum GP of this character. /// public uint MaxGp { get; } /// - /// Gets the current CP of this Chara. + /// Gets the current CP of this character. /// public uint CurrentCp { get; } /// - /// Gets the maximum CP of this Chara. + /// Gets the maximum CP of this character. /// public uint MaxCp { get; } /// - /// Gets the shield percentage of this Chara. + /// Gets the shield percentage of this character. /// public byte ShieldPercentage { get; } /// - /// Gets the ClassJob of this Chara. + /// Gets the ClassJob of this character. /// public RowRef ClassJob { get; } /// - /// Gets the level of this Chara. + /// Gets the level of this character. /// public byte Level { get; } /// - /// Gets a byte array describing the visual appearance of this Chara. + /// Gets a byte array describing the visual appearance of this character. /// Indexed by . /// public byte[] Customize { get; } /// - /// Gets the Free Company tag of this chara. + /// Gets the underlying CustomizeData struct for this character. + /// + public ICustomizeData CustomizeData { get; } + + /// + /// Gets the Free Company tag of this character. /// public SeString CompanyTag { get; } @@ -92,12 +99,12 @@ public interface ICharacter : IGameObject /// Gets the status flags. /// public StatusFlags StatusFlags { get; } - + /// /// Gets the current mount for this character. Will be null if the character doesn't have a mount. /// public RowRef? CurrentMount { get; } - + /// /// Gets the current minion summoned for this character. Will be null if the character doesn't have a minion. /// This method *will* return information about a spawned (but invisible) minion, e.g. if the character is riding a @@ -116,7 +123,7 @@ internal unsafe class Character : GameObject, ICharacter /// This represents a non-static entity. /// /// The address of this character in memory. - internal Character(IntPtr address) + internal Character(nint address) : base(address) { } @@ -155,8 +162,12 @@ internal unsafe class Character : GameObject, ICharacter public byte Level => this.Struct->CharacterData.Level; /// + [Api15ToDo("Do not allocate on each call, use the CS Span and let consumers do allocation if necessary")] public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray(); + /// + public ICustomizeData CustomizeData => new CustomizeData((nint)(&this.Struct->DrawData.CustomizeData)); + /// public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag); @@ -183,14 +194,14 @@ internal unsafe class Character : GameObject, ICharacter (this.Struct->IsAllianceMember ? StatusFlags.AllianceMember : StatusFlags.None) | (this.Struct->IsFriend ? StatusFlags.Friend : StatusFlags.None) | (this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None); - + /// public RowRef? CurrentMount { get { if (this.Struct->IsNotMounted()) return null; // just for safety. - + var mountId = this.Struct->Mount.MountId; return mountId == 0 ? null : LuminaUtils.CreateRef(mountId); } @@ -201,7 +212,7 @@ internal unsafe class Character : GameObject, ICharacter { get { - if (this.Struct->CompanionObject != null) + if (this.Struct->CompanionObject != null) return LuminaUtils.CreateRef(this.Struct->CompanionObject->BaseId); // this is only present if a minion is summoned but hidden (e.g. the player's on a mount). From dc77235c969c56c00fd11827ff48de5724c0ddf2 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 5 Feb 2026 00:37:12 +0100 Subject: [PATCH 448/477] - Fix style cop warnings --- Dalamud/Game/ClientState/Customize/CustomizeData.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Customize/CustomizeData.cs b/Dalamud/Game/ClientState/Customize/CustomizeData.cs index 644bec5e8..5e861db65 100644 --- a/Dalamud/Game/ClientState/Customize/CustomizeData.cs +++ b/Dalamud/Game/ClientState/Customize/CustomizeData.cs @@ -9,7 +9,7 @@ public interface ICustomizeData { /// /// Gets the current race. - /// E.g., Miqo'te, Aura + /// E.g., Miqo'te, Aura. /// public byte Race { get; } @@ -31,13 +31,13 @@ public interface ICustomizeData /// /// Gets the current tribe. - /// E.g., Seeker of the Sun, Keeper of the Moon + /// E.g., Seeker of the Sun, Keeper of the Moon. /// public byte Tribe { get; } /// /// Gets the current face. - /// 1 to 4 + /// 1 to 4. /// public byte Face { get; } @@ -108,7 +108,7 @@ public interface ICustomizeData /// /// Gets the current bust size. - /// 0 to 100 + /// 0 to 100. /// public byte BustSize { get; } From bcf4f396d6d1fd58826c42efe2c842027eba383e Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 5 Feb 2026 01:12:00 +0100 Subject: [PATCH 449/477] - Adjust comments --- Dalamud/Game/ClientState/Customize/CustomizeData.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/ClientState/Customize/CustomizeData.cs b/Dalamud/Game/ClientState/Customize/CustomizeData.cs index 5e861db65..baf8d3a0a 100644 --- a/Dalamud/Game/ClientState/Customize/CustomizeData.cs +++ b/Dalamud/Game/ClientState/Customize/CustomizeData.cs @@ -24,8 +24,7 @@ public interface ICustomizeData public byte BodyType { get; } /// - /// Gets the current height. - /// 0 to 100 + /// Gets the current height (0 to 100). /// public byte Height { get; } @@ -36,8 +35,7 @@ public interface ICustomizeData public byte Tribe { get; } /// - /// Gets the current face. - /// 1 to 4. + /// Gets the current face (1 to 4). /// public byte Face { get; } @@ -102,13 +100,12 @@ public interface ICustomizeData public byte MuscleMass { get; } /// - /// Gets the current tail type. + /// Gets the current tail type (1 to 4). /// public byte TailShape { get; } /// - /// Gets the current bust size. - /// 0 to 100. + /// Gets the current bust size (0 to 100). /// public byte BustSize { get; } From d3b9c75e505c33753e32268b98c8000a03f5bfda Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Feb 2026 17:35:21 +0000 Subject: [PATCH 450/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index cb1f076a6..9ba281cab 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit cb1f076a6fcb6131cd8e1d5bda438adc980b3ee3 +Subproject commit 9ba281cab958049b47bbf7199ab14742c609cd4b From 7d2f12c6e2de70806ed6eea776a0c902577d5877 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 5 Feb 2026 19:56:25 +0100 Subject: [PATCH 451/477] build: 14.0.2.1 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 34b546faf..adaf876ee 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 14.0.2.0 + 14.0.2.1 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From b30a93816b82a5a2ae16a33a1820f45117adbc87 Mon Sep 17 00:00:00 2001 From: Soreepeong <3614868+Soreepeong@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:59:02 +0900 Subject: [PATCH 452/477] Directly work with TexHeader and TextureBuffer --- .../GamePathSharedImmediateTexture.cs | 2 +- .../Textures/Internal/TextureManager.cs | 37 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index 185ae07b9..a320d921e 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -70,7 +70,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture } cancellationToken.ThrowIfCancellationRequested(); - var wrap = tm.NoThrottleCreateFromTexFile(file); + var wrap = tm.NoThrottleCreateFromTexFile(file.Header, file.TextureBuffer); tm.BlameSetName(wrap, this.ToString()); return wrap; } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 982b5c58d..22a257395 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -6,7 +6,6 @@ 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; @@ -20,6 +19,7 @@ using Dalamud.Utility.TerraFxCom; using Lumina.Data; using Lumina.Data.Files; +using Lumina.Data.Parsing.Tex.Buffers; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -219,7 +219,7 @@ internal sealed partial class TextureManager null, _ => Task.FromResult( this.BlameSetName( - this.NoThrottleCreateFromTexFile(file), + this.NoThrottleCreateFromTexFile(file.Header, file.TextureBuffer), debugName ?? $"{nameof(this.CreateFromTexFile)}({ForceNullable(file.FilePath)?.Path})")), cancellationToken); @@ -345,14 +345,14 @@ internal sealed partial class TextureManager /// Creates a texture from the given . Skips the load throttler; intended to be used /// from implementation of s. - /// The data. + /// Header of a .tex file. + /// Texture buffer. /// The loaded texture. - internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) + internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile.TexHeader header, TextureBuffer buffer) { ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); - var buffer = file.TextureBuffer; - var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); + var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(header.Format, false); if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) { @@ -361,34 +361,31 @@ internal sealed partial class TextureManager } var wrap = this.NoThrottleCreateFromRaw(new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({ForceNullable(file.FilePath).Path})"); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({header.Width} x {header.Height})"); return wrap; - - static T? ForceNullable(T s) => s; } /// Creates a texture from the given , trying to interpret it as a /// . /// The file bytes. /// The loaded texture. - internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes) + internal unsafe IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes) { ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes)) throw new InvalidDataException("The file is not a TexFile."); - var bytesArray = fileBytes.ToArray(); - var tf = new TexFile(); - typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( - tf, - [bytesArray]); - typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( - tf, - [new LuminaBinaryReader(bytesArray)]); - // Note: FileInfo and FilePath are not used from TexFile; skip it. + TexFile.TexHeader header; + TextureBuffer buffer; + fixed (byte* p = fileBytes) + { + var lbr = new LuminaBinaryReader(new UnmanagedMemoryStream(p, fileBytes.Length)); + header = lbr.ReadStructure(); + buffer = TextureBuffer.FromStream(header, lbr); + } - var wrap = this.NoThrottleCreateFromTexFile(tf); + var wrap = this.NoThrottleCreateFromTexFile(header, buffer); this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({fileBytes.Length:n0})"); return wrap; } From 0490a71990946a34495fbeb6d9f8005e023d4638 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 7 Feb 2026 20:36:20 +0100 Subject: [PATCH 453/477] Update Network Monitor Widget - Separate checkboxes for up and down tracking - Clarify tracking is for ZoneUp/ZoneDown - Rephrase filter checkbox tooltip --- .../Data/Widgets/NetworkMonitorWidget.cs | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 4460a9f9a..73916761b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -21,10 +21,11 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget { private readonly ConcurrentQueue packets = new(); - private Hook? hookDown; - private Hook? hookUp; + private Hook? hookZoneDown; + private Hook? hookZoneUp; - private bool trackNetwork; + private bool trackZoneUp; + private bool trackZoneDown; private int trackedPackets = 20; private ulong nextPacketIndex; private string filterString = string.Empty; @@ -35,8 +36,8 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget /// Finalizes an instance of the class. ~NetworkMonitorWidget() { - this.hookDown?.Dispose(); - this.hookUp?.Dispose(); + this.hookZoneDown?.Dispose(); + this.hookZoneUp?.Dispose(); } private enum NetworkMessageDirection @@ -60,26 +61,41 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget /// public void Draw() { - this.hookDown ??= Hook.FromAddress( + this.hookZoneDown ??= Hook.FromAddress( (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket, this.OnReceivePacketDetour); - this.hookUp ??= Hook.FromAddress( + this.hookZoneUp ??= Hook.FromAddress( (nint)ZoneClient.MemberFunctionPointers.SendPacket, this.SendPacketDetour); - if (ImGui.Checkbox("Track Network Packets"u8, ref this.trackNetwork)) + if (ImGui.Checkbox("Track ZoneUp"u8, ref this.trackZoneUp)) { - if (this.trackNetwork) + if (this.trackZoneUp) { - this.nextPacketIndex = 0; - this.hookDown?.Enable(); - this.hookUp?.Enable(); + if (!this.trackZoneDown) + this.nextPacketIndex = 0; + + this.hookZoneUp?.Enable(); } else { - this.hookDown?.Disable(); - this.hookUp?.Disable(); + this.hookZoneUp?.Disable(); + } + } + + if (ImGui.Checkbox("Track ZoneDown"u8, ref this.trackZoneDown)) + { + if (this.trackZoneDown) + { + if (!this.trackZoneUp) + this.nextPacketIndex = 0; + + this.hookZoneDown?.Enable(); + } + else + { + this.hookZoneDown?.Disable(); } } @@ -92,6 +108,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget if (ImGui.Button("Clear Stored Packets"u8)) { this.packets.Clear(); + this.nextPacketIndex = 0; } ImGui.SameLine(); @@ -102,7 +119,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.Checkbox("##FilterRecording"u8, ref this.filterRecording); if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Apply filter to incoming packets.\nUncheck to record all packets and filter the table instead."u8); + ImGui.SetTooltip("When enabled, packets are filtered before being recorded.\nWhen disabled, all packets are recorded and filtering only affects packets displayed in the table."u8); ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); ImGuiComponents.HelpMarker("Enter OpCodes in a comma-separated list.\nRanges are supported. Exclude OpCodes with exclamation mark.\nExample: -400,!50-100,650,700-980,!941"); @@ -204,14 +221,14 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget var opCode = *(ushort*)(packet + 2); var targetName = GetTargetName(targetId); this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId, targetName)); - this.hookDown.OriginalDisposeSafe(thisPtr, targetId, packet); + this.hookZoneDown.OriginalDisposeSafe(thisPtr, targetId, packet); } private bool SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, bool a5) { var opCode = *(ushort*)packet; this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty)); - return this.hookUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5); + return this.hookZoneUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5); } private void RecordPacket(NetworkPacketData packet) From 73447f205dc5c3a777f3157e9b4085d7a5004bf6 Mon Sep 17 00:00:00 2001 From: Robert Baker Date: Sat, 7 Feb 2026 20:58:55 -0800 Subject: [PATCH 454/477] Add GPU Info to Crash Handler I'm sure there's a better way to do this, but I also shouldn't be allowed to touch any cpp code. This loops through all dxgi adapters based on example code I ripped from Microsoft and StackOverflow and dumps that into the crash log. I'm hoping it doesn't make the window too tall, so if there's a better way to list only the display adapters that are unique, I'm all for it. --- DalamudCrashHandler/DalamudCrashHandler.cpp | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index f28715dc1..165cce26d 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -28,6 +28,9 @@ #include #include +#include +#pragma comment(lib, "dxgi.lib") + #pragma comment(lib, "comctl32.lib") #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") @@ -470,6 +473,37 @@ void open_folder_and_select_items(HWND hwndOpener, const std::wstring& path) { ILFree(piid); } +std::vector EnumerateAdapters(void) +{ + IDXGIAdapter1* pAdapter; + std::vector vAdapters; + IDXGIFactory1* pFactory = NULL; + + + // Create a DXGIFactory object. + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&pFactory))) + { + return vAdapters; + } + + + for (UINT i = 0; + pFactory->EnumAdapters1(i, &pAdapter) != DXGI_ERROR_NOT_FOUND; + ++i) + { + vAdapters.push_back(pAdapter); + } + + + if (pFactory) + { + pFactory->Release(); + } + + return vAdapters; + +} + void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const std::string& crashLog, const std::string& troubleshootingPackData) { static const char* SourceLogFiles[] = { "output.log", // XIVLauncher for Windows @@ -1022,6 +1056,27 @@ int main() { log << std::format(L"System Time: {0:%F} {0:%T} {0:%Ez}", std::chrono::system_clock::now()) << std::endl; log << std::format(L"CPU Vendor: {}", vendor) << std::endl; log << std::format(L"CPU Brand: {}", brand) << std::endl; + + std::vector availableAdapters = EnumerateAdapters(); + + for (int i = 0; i < availableAdapters.size(); i++) { + auto& myAdapter = *availableAdapters[i]; + auto adapterDescription = DXGI_ADAPTER_DESC1(); + myAdapter.GetDesc1(&adapterDescription); + // Print description to console here + log << std::format(L"GPU Desc: {}", adapterDescription.Description) << std::endl; + } + + /* + for_each(availableAdapters.begin(), availableAdapters.end(), [](IDXGIAdapter1* adapter, , std::wostream log) { + auto& myAdapter = *adapter; + auto adapterDescription = DXGI_ADAPTER_DESC1(); + myAdapter.GetDesc1(&adapterDescription); + // Print description to console here + log << std::format(L"GPU Desc: {}", adapterDescription.Description) << std::endl; + }); + */ + log << L"\n" << stackTrace << std::endl; if (pProgressDialog) From 34f13b3823f4f114742f5eadd04db74418016281 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 8 Feb 2026 06:57:53 +0000 Subject: [PATCH 455/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 9ba281cab..28421f946 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9ba281cab958049b47bbf7199ab14742c609cd4b +Subproject commit 28421f946ae9ec262630f04b2b4a114b54527ff9 From 78912c155215a05faf5c37252b3f8181ffa389ba Mon Sep 17 00:00:00 2001 From: Glorou Date: Sun, 8 Feb 2026 21:56:48 -0500 Subject: [PATCH 456/477] Init --- Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs | 1 + Dalamud/Interface/ImGuiFileDialog/FileDialog.cs | 2 ++ .../Interface/ImGuiFileDialog/FileDialogManager.cs | 14 ++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index ef886e957..484b883ee 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -648,6 +648,7 @@ public partial class FileDialog private void AddFileNameInSelection(string name, bool setLastSelection) { this.selectedFileNames.Add(name); + this.SelectionChanged(this, this.GetFilePathName()); if (this.selectedFileNames.Count == 1) { this.fileNameBuffer = name; diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs index e33fc2fc4..b9ac634ab 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs @@ -97,6 +97,8 @@ public partial class FileDialog this.SetupSideBar(); } + public event EventHandler? SelectionChanged; + /// /// Shows the dialog. /// diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index ee12e7424..eca65cd72 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using Dalamud.Bindings.ImGui; +using FFXIVClientStructs; + namespace Dalamud.Interface.ImGuiFileDialog; /// @@ -25,11 +27,13 @@ public class FileDialogManager #pragma warning restore SA1401 #pragma warning restore SA1201 + public event EventHandler? SelectionChanged; private FileDialog? dialog; private Action? callback; private Action>? multiCallback; private string savedPath = "."; + /// /// Create a dialog which selects an already existing folder. /// @@ -175,6 +179,13 @@ public class FileDialogManager this.multiCallback = null; } + public string? GetCurrentPath() + { + return this.dialog?.GetCurrentPath(); + } + + private void OnSelectionChange(object sender, string path) => this.SelectionChanged(sender, path); + private void SetDialog( string id, string title, @@ -200,9 +211,11 @@ public class FileDialogManager if (this.dialog is not null) { this.dialog.SortOrderChanged -= this.OnSortOrderChange; + this.dialog.SelectionChanged -= this.OnSelectionChange; } this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); + if (this.GetDefaultSortOrder is not null) { try @@ -217,6 +230,7 @@ public class FileDialogManager } this.dialog.SortOrderChanged += this.OnSortOrderChange; + this.dialog.SelectionChanged += this.OnSelectionChange; this.dialog.WindowFlags |= this.AddedWindowFlags; foreach (var (name, location, icon, position) in this.CustomSideBarItems) this.dialog.SetQuickAccess(name, location, icon, position); From 332d0d0cf5bc143401f6a0032d54bb0ea0d7390c Mon Sep 17 00:00:00 2001 From: Glorou Date: Mon, 9 Feb 2026 10:38:14 -0500 Subject: [PATCH 457/477] Tweaked --- Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs | 3 +-- Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index 484b883ee..289203d59 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -648,7 +648,6 @@ public partial class FileDialog private void AddFileNameInSelection(string name, bool setLastSelection) { this.selectedFileNames.Add(name); - this.SelectionChanged(this, this.GetFilePathName()); if (this.selectedFileNames.Count == 1) { this.fileNameBuffer = name; @@ -657,7 +656,7 @@ public partial class FileDialog { this.fileNameBuffer = $"{this.selectedFileNames.Count} files Selected"; } - + this.SelectionChanged(this, this.GetFilePathName()); if (setLastSelection) { this.lastSelectedFileName = name; diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index eca65cd72..5e0a03b66 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -27,12 +27,13 @@ public class FileDialogManager #pragma warning restore SA1401 #pragma warning restore SA1201 - public event EventHandler? SelectionChanged; + private FileDialog? dialog; private Action? callback; private Action>? multiCallback; private string savedPath = "."; + public event EventHandler? SelectionChanged; /// /// Create a dialog which selects an already existing folder. @@ -184,7 +185,7 @@ public class FileDialogManager return this.dialog?.GetCurrentPath(); } - private void OnSelectionChange(object sender, string path) => this.SelectionChanged(sender, path); + private void OnSelectionChange(object sender, string path) => this.SelectionChanged?.Invoke(sender, path); private void SetDialog( string id, From 256ab9dc9c11d0c73a9449dbf8994ed7a75f89e9 Mon Sep 17 00:00:00 2001 From: Glorou Date: Mon, 9 Feb 2026 10:38:32 -0500 Subject: [PATCH 458/477] add whitespace --- Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index 289203d59..b1fc6e049 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -656,6 +656,7 @@ public partial class FileDialog { this.fileNameBuffer = $"{this.selectedFileNames.Count} files Selected"; } + this.SelectionChanged(this, this.GetFilePathName()); if (setLastSelection) { From 8285aa1014777285e418a49c79a9547c90133840 Mon Sep 17 00:00:00 2001 From: Glorou <93338547+Glorou@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:50:46 -0500 Subject: [PATCH 459/477] Clean up FileDialogManager by removing unused code Removed unused GetCurrentPath method and unnecessary using directives. --- Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index 5e0a03b66..8cf0baa2c 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using Dalamud.Bindings.ImGui; -using FFXIVClientStructs; - namespace Dalamud.Interface.ImGuiFileDialog; /// @@ -27,7 +25,6 @@ public class FileDialogManager #pragma warning restore SA1401 #pragma warning restore SA1201 - private FileDialog? dialog; private Action? callback; private Action>? multiCallback; @@ -180,11 +177,6 @@ public class FileDialogManager this.multiCallback = null; } - public string? GetCurrentPath() - { - return this.dialog?.GetCurrentPath(); - } - private void OnSelectionChange(object sender, string path) => this.SelectionChanged?.Invoke(sender, path); private void SetDialog( From e2297661f30c852932aee1b000002aee4bd91e0a Mon Sep 17 00:00:00 2001 From: Glorou Date: Mon, 9 Feb 2026 11:17:11 -0500 Subject: [PATCH 460/477] Added doc --- Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index 8cf0baa2c..7332cd735 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -30,6 +30,10 @@ public class FileDialogManager private Action>? multiCallback; private string savedPath = "."; + /// + /// Event fires when a new file is selected by the user + /// + /// Returns the path of the file as a string public event EventHandler? SelectionChanged; /// @@ -208,7 +212,6 @@ public class FileDialogManager } this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); - if (this.GetDefaultSortOrder is not null) { try From 3de8c511bf3990e7efa9eb049df60e9e02cf9e79 Mon Sep 17 00:00:00 2001 From: balloon41 Date: Tue, 10 Feb 2026 13:37:58 -0600 Subject: [PATCH 461/477] Update IPlayerState.cs (#2617) Fixed type in BaseRestedExperience summary --- Dalamud/Plugin/Services/IPlayerState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index 21d88010b..838d5a346 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -159,7 +159,7 @@ public interface IPlayerState : IDalamudService RowRef FreeAetheryte { get; } /// - /// Gets the amount of received player commendations of the local player. + /// Gets the amount of rested experience available to the local player. /// uint BaseRestedExperience { get; } From 0a070970a07fd9f3c22bd03c6fd254a68cdc374f Mon Sep 17 00:00:00 2001 From: marzent Date: Wed, 11 Feb 2026 13:03:32 +0100 Subject: [PATCH 462/477] Fix troubleshooting json error on non-Windows platforms --- Dalamud/Support/Troubleshooting.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index f9e084db8..779754ee8 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -90,8 +90,7 @@ public static class Troubleshooting File.WriteAllText( Path.Join( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "XIVLauncher", + startInfo.LogPath, "dalamud.troubleshooting.json"), JsonConvert.SerializeObject(payload, Formatting.Indented)); } From 49e281e57376da85346be3fb97f402ba4c020cfa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Feb 2026 19:10:35 +0000 Subject: [PATCH 463/477] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 28421f946..a97e9f89d 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 28421f946ae9ec262630f04b2b4a114b54527ff9 +Subproject commit a97e9f89d72d40eabd0f3b52266862dca3eba872 From abe27891c3ed07bf7be73bc7f19221945108afcc Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:45:23 +0100 Subject: [PATCH 464/477] Tidy tidy --- DalamudCrashHandler/DalamudCrashHandler.cpp | 32 +++++---------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 165cce26d..81b37992b 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -473,20 +473,17 @@ void open_folder_and_select_items(HWND hwndOpener, const std::wstring& path) { ILFree(piid); } -std::vector EnumerateAdapters(void) +std::vector enum_dxgi_adapters() { - IDXGIAdapter1* pAdapter; - std::vector vAdapters; + std::vector vAdapters; + IDXGIFactory1* pFactory = NULL; - - - // Create a DXGIFactory object. if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&pFactory))) { return vAdapters; } - + IDXGIAdapter1* pAdapter; for (UINT i = 0; pFactory->EnumAdapters1(i, &pAdapter) != DXGI_ERROR_NOT_FOUND; ++i) @@ -501,7 +498,6 @@ std::vector EnumerateAdapters(void) } return vAdapters; - } void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const std::string& crashLog, const std::string& troubleshootingPackData) { @@ -1057,26 +1053,12 @@ int main() { log << std::format(L"CPU Vendor: {}", vendor) << std::endl; log << std::format(L"CPU Brand: {}", brand) << std::endl; - std::vector availableAdapters = EnumerateAdapters(); - - for (int i = 0; i < availableAdapters.size(); i++) { - auto& myAdapter = *availableAdapters[i]; - auto adapterDescription = DXGI_ADAPTER_DESC1(); - myAdapter.GetDesc1(&adapterDescription); - // Print description to console here + for (IDXGIAdapter1* adapter : enum_dxgi_adapters()) { + DXGI_ADAPTER_DESC1 adapterDescription{}; + myAdapter->GetDesc1(&adapterDescription); log << std::format(L"GPU Desc: {}", adapterDescription.Description) << std::endl; } - /* - for_each(availableAdapters.begin(), availableAdapters.end(), [](IDXGIAdapter1* adapter, , std::wostream log) { - auto& myAdapter = *adapter; - auto adapterDescription = DXGI_ADAPTER_DESC1(); - myAdapter.GetDesc1(&adapterDescription); - // Print description to console here - log << std::format(L"GPU Desc: {}", adapterDescription.Description) << std::endl; - }); - */ - log << L"\n" << stackTrace << std::endl; if (pProgressDialog) From b1b99bae134b76c954193739a1a848ee30e16d44 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:03:38 +0100 Subject: [PATCH 465/477] Use correct variable name --- DalamudCrashHandler/DalamudCrashHandler.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 81b37992b..f8cadd82b 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -491,7 +491,6 @@ std::vector enum_dxgi_adapters() vAdapters.push_back(pAdapter); } - if (pFactory) { pFactory->Release(); @@ -1055,7 +1054,7 @@ int main() { for (IDXGIAdapter1* adapter : enum_dxgi_adapters()) { DXGI_ADAPTER_DESC1 adapterDescription{}; - myAdapter->GetDesc1(&adapterDescription); + adapter->GetDesc1(&adapterDescription); log << std::format(L"GPU Desc: {}", adapterDescription.Description) << std::endl; } From b963e83cbab333a37e875f93c52fe21c3b25c3f3 Mon Sep 17 00:00:00 2001 From: Critical Impact Date: Fri, 13 Feb 2026 19:04:33 +1000 Subject: [PATCH 466/477] Use effective working ID + internal name --- .../Windows/Data/Widgets/DataShareWidget.cs | 12 ++--- Dalamud/Plugin/DalamudPluginInterface.cs | 8 ++-- .../Ipc/Exceptions/DataCacheCreationError.cs | 8 ++-- .../Exceptions/DataCacheTypeMismatchError.cs | 8 ++-- Dalamud/Plugin/Ipc/Internal/DataCache.cs | 44 +++++++++++-------- .../Plugin/Ipc/Internal/DataCachePluginId.cs | 16 +++++++ Dalamud/Plugin/Ipc/Internal/DataShare.cs | 30 ++++++------- 7 files changed, 76 insertions(+), 50 deletions(-) create mode 100644 Dalamud/Plugin/Ipc/Internal/DataCachePluginId.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 827d609a7..ebb5b6581 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -87,7 +87,7 @@ internal class DataShareWidget : IDataWindowWidget try { var dataShare = Service.Get(); - var data2 = dataShare.GetData(name, "DataShareWidget"); + var data2 = dataShare.GetData(name, new DataCachePluginId("DataShareWidget", Guid.Empty)); try { data = Encoding.UTF8.GetBytes( @@ -98,7 +98,7 @@ internal class DataShareWidget : IDataWindowWidget } finally { - dataShare.RelinquishData(name, "DataShareWidget"); + dataShare.RelinquishData(name, new DataCachePluginId("DataShareWidget", Guid.Empty)); } } catch (Exception e) @@ -284,7 +284,7 @@ internal class DataShareWidget : IDataWindowWidget ImGui.TableSetupColumn("Shared Tag"u8); ImGui.TableSetupColumn("Show"u8); - ImGui.TableSetupColumn("Creator Internal Name"u8); + ImGui.TableSetupColumn("Creator"u8); ImGui.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("Consumers"u8); ImGui.TableHeadersRow(); @@ -312,9 +312,9 @@ internal class DataShareWidget : IDataWindowWidget this.nextTab = 2 + index; } - this.DrawTextCell(share.CreatorAssembly, null, true); - this.DrawTextCell(share.Users.Length.ToString(), null, true); - this.DrawTextCell(string.Join(", ", share.Users), null, true); + this.DrawTextCell(share.CreatorPluginId.InternalName, () => share.CreatorPluginId.EffectiveWorkingId.ToString(), true); + this.DrawTextCell(share.UserPluginIds.Length.ToString(), null, true); + this.DrawTextCell(string.Join(", ", share.UserPluginIds.Select(c => c.InternalName)), () => string.Join("\n", share.UserPluginIds.Select(c => $"{c.InternalName} ({c.EffectiveWorkingId.ToString()}")), true); } } } diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 28affddb4..48f91b250 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -227,19 +227,19 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// public T GetOrCreateData(string tag, Func dataGenerator) where T : class - => Service.Get().GetOrCreateData(tag, this.plugin.InternalName, dataGenerator); + => Service.Get().GetOrCreateData(tag, new DataCachePluginId(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId), dataGenerator); /// public void RelinquishData(string tag) - => Service.Get().RelinquishData(tag, this.plugin.InternalName); + => Service.Get().RelinquishData(tag, new DataCachePluginId(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId)); /// public bool TryGetData(string tag, [NotNullWhen(true)] out T? data) where T : class - => Service.Get().TryGetData(tag, this.plugin.InternalName, out data); + => Service.Get().TryGetData(tag, new DataCachePluginId(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId), out data); /// public T? GetData(string tag) where T : class - => Service.Get().GetData(tag, this.plugin.InternalName); + => Service.Get().GetData(tag, new DataCachePluginId(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId)); /// public ICallGateProvider GetIpcProvider(string name) diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs index db095bad9..38b729616 100644 --- a/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Ipc.Internal; + namespace Dalamud.Plugin.Ipc.Exceptions; /// @@ -9,11 +11,11 @@ public class DataCacheCreationError : IpcError /// Initializes a new instance of the class. /// /// Tag of the data cache. - /// The assembly name of the caller. + /// The plugin ID of the creating plugin. /// The type expected. /// The thrown exception. - public DataCacheCreationError(string tag, string creator, Type expectedType, Exception ex) - : base($"The creation of the {expectedType} data cache {tag} initialized by {creator} was unsuccessful.", ex) + public DataCacheCreationError(string tag, DataCachePluginId creatorPluginId, Type expectedType, Exception ex) + : base($"The creation of the {expectedType} data cache {tag} initialized by {creatorPluginId.InternalName} ({creatorPluginId.EffectiveWorkingId}) was unsuccessful.", ex) { } } diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs index e5d9cc4db..bfe09b120 100644 --- a/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Ipc.Internal; + namespace Dalamud.Plugin.Ipc.Exceptions; /// @@ -9,11 +11,11 @@ public class DataCacheTypeMismatchError : IpcError /// Initializes a new instance of the class. /// /// Tag of the data cache. - /// Assembly name of the plugin creating the cache. + /// The plugin ID of the creating plugin. /// The requested type. /// The stored type. - public DataCacheTypeMismatchError(string tag, string creator, Type requestedType, Type actualType) - : base($"Data cache {tag} was requested with type {requestedType}, but {creator} created type {actualType}.") + public DataCacheTypeMismatchError(string tag, DataCachePluginId creatorPluginId, Type requestedType, Type actualType) + : base($"Data cache {tag} was requested with type {requestedType}, but {creatorPluginId.InternalName} ({creatorPluginId.EffectiveWorkingId}) created type {actualType}.") { } } diff --git a/Dalamud/Plugin/Ipc/Internal/DataCache.cs b/Dalamud/Plugin/Ipc/Internal/DataCache.cs index d565c8b35..cbb5bb342 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataCache.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.ExceptionServices; using Dalamud.Plugin.Ipc.Exceptions; @@ -16,12 +17,12 @@ internal readonly struct DataCache /// Name of the data. internal readonly string Tag; - /// The assembly name of the initial creator. - internal readonly string CreatorAssemblyName; + /// The creating plugin ID of this DataCache entry. + internal readonly DataCachePluginId CreatorPluginId; - /// A not-necessarily distinct list of current users. + /// A distinct list of plugin IDs that are using this data. /// Also used as a reference count tracker. - internal readonly List UserAssemblyNames; + internal readonly List UserPluginIds; /// The type the data was registered as. internal readonly Type Type; @@ -33,14 +34,14 @@ internal readonly struct DataCache /// Initializes a new instance of the struct. /// /// Name of the data. - /// The assembly name of the initial creator. + /// The internal name and effective working ID of the creating plugin. /// A reference to data. /// The type of the data. - public DataCache(string tag, string creatorAssemblyName, object? data, Type type) + public DataCache(string tag, DataCachePluginId creatorPluginId, object? data, Type type) { this.Tag = tag; - this.CreatorAssemblyName = creatorAssemblyName; - this.UserAssemblyNames = []; + this.CreatorPluginId = creatorPluginId; + this.UserPluginIds = []; this.Data = data; this.Type = type; } @@ -49,40 +50,40 @@ internal readonly struct DataCache /// Creates a new instance of the struct, using the given data generator function. /// /// The name for the data cache. - /// The assembly name of the initial creator. + /// The internal name and effective working ID of the creating plugin. /// The function that generates the data if it does not already exist. /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The new instance of . - public static DataCache From(string tag, string creatorAssemblyName, Func dataGenerator) + public static DataCache From(string tag, DataCachePluginId creatorPluginId, Func dataGenerator) where T : class { try { - var result = new DataCache(tag, creatorAssemblyName, dataGenerator.Invoke(), typeof(T)); + var result = new DataCache(tag, creatorPluginId, dataGenerator.Invoke(), typeof(T)); Log.Verbose( "[{who}] Created new data for [{Tag:l}] for creator {Creator:l}.", nameof(DataShare), tag, - creatorAssemblyName); + creatorPluginId); return result; } catch (Exception e) { throw ExceptionDispatchInfo.SetCurrentStackTrace( - new DataCacheCreationError(tag, creatorAssemblyName, typeof(T), e)); + new DataCacheCreationError(tag, creatorPluginId, typeof(T), e)); } } /// /// Attempts to fetch the data. /// - /// The name of the caller assembly. + /// The calling plugin ID. /// The value, if succeeded. /// The exception, if failed. /// Desired type of the data. /// true on success. public bool TryGetData( - string callerName, + DataCachePluginId callingPluginId, [NotNullWhen(true)] out T? value, [NotNullWhen(false)] out Exception? ex) where T : class @@ -98,16 +99,21 @@ internal readonly struct DataCache value = data; ex = null; - // Register the access history - lock (this.UserAssemblyNames) - this.UserAssemblyNames.Add(callerName); + // Register the access history. The effective working ID is unique per plugin and persists between reloads, so only add it once. + lock (this.UserPluginIds) + { + if (this.UserPluginIds.All(c => c.EffectiveWorkingId != callingPluginId.EffectiveWorkingId)) + { + this.UserPluginIds.Add(callingPluginId); + } + } return true; default: value = null; ex = ExceptionDispatchInfo.SetCurrentStackTrace( - new DataCacheTypeMismatchError(this.Tag, this.CreatorAssemblyName, typeof(T), this.Type)); + new DataCacheTypeMismatchError(this.Tag, this.CreatorPluginId, typeof(T), this.Type)); return false; } } diff --git a/Dalamud/Plugin/Ipc/Internal/DataCachePluginId.cs b/Dalamud/Plugin/Ipc/Internal/DataCachePluginId.cs new file mode 100644 index 000000000..c68dc7c06 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Internal/DataCachePluginId.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; + +using Dalamud.Plugin.Ipc.Exceptions; + +using Serilog; + +namespace Dalamud.Plugin.Ipc.Internal; + +/// +/// Stores the internal name and effective working ID of a plugin accessing datashare. +/// +/// The internal name of the plugin. +/// The effective working ID of the plugin. +public record DataCachePluginId(string InternalName, Guid EffectiveWorkingId); diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index f71d8d2c2..ffad4876e 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -33,23 +33,23 @@ internal class DataShare : IServiceType /// /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The name for the data cache. - /// The name of the caller. + /// The ID of the calling plugin. /// The function that generates the data if it does not already exist. /// Either the existing data for or the data generated by . /// Thrown if a cache for exists, but contains data of a type not assignable to . /// Thrown if the stored data for a cache is null. /// Thrown if throws an exception or returns null. - public T GetOrCreateData(string tag, string callerName, Func dataGenerator) + public T GetOrCreateData(string tag, DataCachePluginId callingPluginId, Func dataGenerator) where T : class { Lazy cacheLazy; lock (this.caches) { if (!this.caches.TryGetValue(tag, out cacheLazy)) - this.caches[tag] = cacheLazy = new(() => DataCache.From(tag, callerName, dataGenerator)); + this.caches[tag] = cacheLazy = new(() => DataCache.From(tag, callingPluginId, dataGenerator)); } - return cacheLazy.Value.TryGetData(callerName, out var value, out var ex) ? value : throw ex; + return cacheLazy.Value.TryGetData(callingPluginId, out var value, out var ex) ? value : throw ex; } /// @@ -57,8 +57,8 @@ internal class DataShare : IServiceType /// If no assembly uses the data anymore, the cache will be removed from the data share and if it is an IDisposable, Dispose will be called on it. /// /// The name for the data cache. - /// The name of the caller. - public void RelinquishData(string tag, string callerName) + /// The ID of the calling plugin. + public void RelinquishData(string tag, DataCachePluginId callingPluginId) { DataCache cache; lock (this.caches) @@ -67,7 +67,7 @@ internal class DataShare : IServiceType return; cache = cacheLazy.Value; - if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) + if (!cache.UserPluginIds.Remove(callingPluginId) || cache.UserPluginIds.Count > 0) return; if (!this.caches.Remove(tag)) return; @@ -97,10 +97,10 @@ internal class DataShare : IServiceType /// /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The name for the data cache. - /// The name of the caller. + /// The ID of the calling plugin. /// The requested data on success, null otherwise. /// True if the requested data exists and is assignable to the requested type. - public bool TryGetData(string tag, string callerName, [NotNullWhen(true)] out T? data) + public bool TryGetData(string tag, DataCachePluginId callingPluginId, [NotNullWhen(true)] out T? data) where T : class { data = null; @@ -111,7 +111,7 @@ internal class DataShare : IServiceType return false; } - return cacheLazy.Value.TryGetData(callerName, out data, out _); + return cacheLazy.Value.TryGetData(callingPluginId, out data, out _); } /// @@ -120,12 +120,12 @@ internal class DataShare : IServiceType /// /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The name for the data cache. - /// The name of the caller. + /// The ID of the calling plugin. /// The requested data. /// Thrown if is not registered. /// Thrown if a cache for exists, but contains data of a type not assignable to . /// Thrown if the stored data for a cache is null. - public T GetData(string tag, string callerName) + public T GetData(string tag, DataCachePluginId callingPluginId) where T : class { Lazy cacheLazy; @@ -135,19 +135,19 @@ internal class DataShare : IServiceType throw new KeyNotFoundException($"The data cache [{tag}] is not registered."); } - return cacheLazy.Value.TryGetData(callerName, out var value, out var ex) ? value : throw ex; + return cacheLazy.Value.TryGetData(callingPluginId, out var value, out var ex) ? value : throw ex; } /// /// Obtain a read-only list of data shares. /// /// All currently subscribed tags, their creator names and all their users. - internal IEnumerable<(string Tag, string CreatorAssembly, string[] Users)> GetAllShares() + internal IEnumerable<(string Tag, DataCachePluginId CreatorPluginId, DataCachePluginId[] UserPluginIds)> GetAllShares() { lock (this.caches) { return this.caches.Select( - kvp => (kvp.Key, kvp.Value.Value.CreatorAssemblyName, kvp.Value.Value.UserAssemblyNames.ToArray())); + kvp => (kvp.Key, kvp.Value.Value.CreatorPluginId, kvp.Value.Value.UserPluginIds.ToArray())); } } } From 907b585b75542d72182276da0a53a53b48c266ea Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 30 Jan 2026 12:18:39 +0100 Subject: [PATCH 467/477] Add support for Achievements to IUnlockState --- Dalamud/Game/UnlockState/UnlockState.cs | 80 +++++++++++++++++++++---- Dalamud/Plugin/Services/IUnlockState.cs | 15 ++++- 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 5ccd7fadb..8496c898a 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Dalamud.Data; using Dalamud.Game.Gui; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -16,7 +17,9 @@ using FFXIVClientStructs.FFXIV.Component.Exd; using Lumina.Excel; using Lumina.Excel.Sheets; +using AchievementSheet = Lumina.Excel.Sheets.Achievement; using ActionSheet = Lumina.Excel.Sheets.Action; +using CSAchievement = FFXIVClientStructs.FFXIV.Client.Game.UI.Achievement; using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent; using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; @@ -30,7 +33,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState { private static readonly ModuleLog Log = new(nameof(UnlockState)); - private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; + [ServiceManager.ServiceDependency] + private readonly TargetSigScanner sigScanner = Service.Get(); [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service.Get(); @@ -44,17 +48,31 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState [ServiceManager.ServiceDependency] private readonly RecipeData recipeData = Service.Get(); + private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; + private readonly Hook setAchievementCompletedHook; + [ServiceManager.ServiceConstructor] private UnlockState() { this.clientState.Login += this.OnLogin; this.clientState.Logout += this.OnLogout; this.gameGui.AgentUpdate += this.OnAgentUpdate; + + this.setAchievementCompletedHook = Hook.FromAddress( + this.sigScanner.ScanText("81 FA ?? ?? ?? ?? 0F 87 ?? ?? ?? ?? 53"), + this.SetAchievementCompletedDetour); + + this.setAchievementCompletedHook.Enable(); } + private delegate void SetAchievementCompletedDelegate(CSAchievement* thisPtr, uint id); + /// public event IUnlockState.UnlockDelegate Unlock; + /// + public bool IsAchievementListLoaded => CSAchievement.Instance()->IsLoaded(); + private bool IsLoaded => PlayerState.Instance()->IsLoaded; /// @@ -63,6 +81,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.clientState.Login -= this.OnLogin; this.clientState.Logout -= this.OnLogout; this.gameGui.AgentUpdate -= this.OnAgentUpdate; + + this.setAchievementCompletedHook.Dispose(); + } + + /// + public bool IsAchievementComplete(AchievementSheet row) + { + // Only check for login state here as individual Achievements + // may be flagged as complete when you unlock them, regardless + // of whether the full Achievements list was loaded or not. + + if (!this.IsLoaded) + return false; + + return CSAchievement.Instance()->IsComplete((int)row.RowId); } /// @@ -464,6 +497,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (!this.IsLoaded || rowRef.IsUntyped) return false; + if (rowRef.TryGetValue(out var achievementRow)) + return this.IsAchievementComplete(achievementRow); + if (rowRef.TryGetValue(out var actionRow)) return this.IsActionUnlocked(actionRow); @@ -621,6 +657,16 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.Update(); } + private void SetAchievementCompletedDetour(CSAchievement* thisPtr, uint id) + { + this.setAchievementCompletedHook.Original(thisPtr, id); + + if (!this.IsLoaded) + return; + + this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef(id)); + } + private void Update() { if (!this.IsLoaded) @@ -628,6 +674,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState Log.Verbose("Checking for new unlocks..."); + // Do not check for Achievements here! + this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); @@ -688,7 +736,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - EmjCostume // Probably not happening, because it requires fetching data from server: - // - Achievements // - Titles // - Bozjan Field Notes // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 @@ -712,16 +759,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}"); - foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + this.RaiseUnlockSafely((RowRef)rowRef); + } + } + + private void RaiseUnlockSafely(RowRef rowRef) + { + foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + { + try { - try - { - action((RowRef)rowRef); - } - catch (Exception ex) - { - Log.Error(ex, "Exception during raise of {handler}", action.Method); - } + action(rowRef); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); } } } @@ -751,6 +803,12 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// public event IUnlockState.UnlockDelegate? Unlock; + /// + public bool IsAchievementListLoaded => this.unlockStateService.IsAchievementListLoaded; + + /// + public bool IsAchievementComplete(AchievementSheet row) => this.unlockStateService.IsAchievementComplete(row); + /// public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index f51222ba1..29e5186bc 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - using Lumina.Excel; using Lumina.Excel.Sheets; @@ -23,6 +21,19 @@ public interface IUnlockState : IDalamudService /// event UnlockDelegate? Unlock; + /// + /// Gets a value indicating whether the full Achievements list was received. + /// + bool IsAchievementListLoaded { get; } + + /// + /// Determines whether the specified Achievement is completed.
+ /// Requires that the player requested the Achievements list (can be chcked with ). + ///
+ /// The Achievement row to check. + /// if completed; otherwise, . + bool IsAchievementComplete(Achievement row); + /// /// Determines whether the specified Action is unlocked. /// From 1ba18e54bf14fa6475001fa55bbcfab6c42249a2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 30 Jan 2026 12:28:05 +0100 Subject: [PATCH 468/477] Add support for Titles to IUnlockState --- Dalamud/Game/UnlockState/UnlockState.cs | 48 +++++++++++++++++++++++-- Dalamud/Plugin/Services/IUnlockState.cs | 13 +++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 8496c898a..273a0228e 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -50,6 +50,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; private readonly Hook setAchievementCompletedHook; + private readonly Hook setTitleUnlockedHook; [ServiceManager.ServiceConstructor] private UnlockState() @@ -62,17 +63,27 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.sigScanner.ScanText("81 FA ?? ?? ?? ?? 0F 87 ?? ?? ?? ?? 53"), this.SetAchievementCompletedDetour); + this.setTitleUnlockedHook = Hook.FromAddress( + this.sigScanner.ScanText("B8 ?? ?? ?? ?? 66 3B D0 73 ?? 44 0F B7 C2 49 C1 E8 ?? 4C 03 C1 0F B7 C2 83 E0 ?? 41 0F B6 48 ?? 0F AB C1"), + this.SetTitleUnlockedDetour); + this.setAchievementCompletedHook.Enable(); + this.setTitleUnlockedHook.Enable(); } private delegate void SetAchievementCompletedDelegate(CSAchievement* thisPtr, uint id); + private delegate void SetTitleUnlockedDelegate(TitleList* thisPtr, ushort id); + /// public event IUnlockState.UnlockDelegate Unlock; /// public bool IsAchievementListLoaded => CSAchievement.Instance()->IsLoaded(); + /// + public bool IsTitleListLoaded => UIState.Instance()->TitleList.DataReceived; + private bool IsLoaded => PlayerState.Instance()->IsLoaded; /// @@ -448,6 +459,19 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId); } + /// + public bool IsTitleUnlocked(Title row) + { + // Only check for login state here as individual Titles + // may be flagged as complete when you unlock them, regardless + // of whether the full Titles list was loaded or not. + + if (!this.IsLoaded) + return false; + + return UIState.Instance()->TitleList.IsTitleUnlocked((ushort)row.RowId); + } + /// public bool IsTraitUnlocked(Trait row) { @@ -608,6 +632,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (rowRef.TryGetValue(out var secretRecipeBookRow)) return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow); + if (rowRef.TryGetValue(out var titleRow)) + return this.IsTitleUnlocked(titleRow); + if (rowRef.TryGetValue<Trait>(out var traitRow)) return this.IsTraitUnlocked(traitRow); @@ -667,14 +694,24 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef<AchievementSheet>(id)); } + private void SetTitleUnlockedDetour(TitleList* thisPtr, ushort id) + { + this.setTitleUnlockedHook.Original(thisPtr, id); + + if (!this.IsLoaded) + return; + + this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef<Title>(id)); + } + private void Update() { if (!this.IsLoaded) return; Log.Verbose("Checking for new unlocks..."); - - // Do not check for Achievements here! + + // Do not check for Achievements or Titles here! this.UpdateUnlocksForSheet<ActionSheet>(); this.UpdateUnlocksForSheet<AetherCurrent>(); @@ -736,7 +773,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - EmjCostume // Probably not happening, because it requires fetching data from server: - // - Titles // - Bozjan Field Notes // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 } @@ -806,6 +842,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// <inheritdoc/> public bool IsAchievementListLoaded => this.unlockStateService.IsAchievementListLoaded; + /// <inheritdoc/> + public bool IsTitleListLoaded => this.unlockStateService.IsTitleListLoaded; + /// <inheritdoc/> public bool IsAchievementComplete(AchievementSheet row) => this.unlockStateService.IsAchievementComplete(row); @@ -932,6 +971,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// <inheritdoc/> public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row); + /// <inheritdoc/> + public bool IsTitleUnlocked(Title row) => this.unlockStateService.IsTitleUnlocked(row); + /// <inheritdoc/> public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 29e5186bc..4554aa318 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -26,6 +26,11 @@ public interface IUnlockState : IDalamudService /// </summary> bool IsAchievementListLoaded { get; } + /// <summary> + /// Gets a value indicating whether the full Titles list was received. + /// </summary> + bool IsTitleListLoaded { get; } + /// <summary> /// Determines whether the specified Achievement is completed.<br/> /// Requires that the player requested the Achievements list (can be chcked with <see cref="IsAchievementListLoaded"/>). @@ -322,6 +327,14 @@ public interface IUnlockState : IDalamudService /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> bool IsSecretRecipeBookUnlocked(SecretRecipeBook row); + /// <summary> + /// Determines whether the specified Title is unlocked.<br/> + /// Requires that the player requested the Titles list (can be chcked with <see cref="IsTitleListLoaded"/>). + /// </summary> + /// <param name="row">The Title row to check.</param> + /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> + bool IsTitleUnlocked(Title row); + /// <summary> /// Determines whether the specified Trait is unlocked. /// </summary> From 46513978087cc11b647fa6a39552be986dd96819 Mon Sep 17 00:00:00 2001 From: Haselnussbomber <mail@haselnussbomber.de> Date: Fri, 30 Jan 2026 12:57:08 +0100 Subject: [PATCH 469/477] Add support for Adventures to IUnlockState --- Dalamud/Game/UnlockState/UnlockState.cs | 19 +++++++++++++++++-- Dalamud/Plugin/Services/IUnlockState.cs | 7 +++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 273a0228e..7e8f64adf 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -115,6 +115,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId); } + /// <inheritdoc/> + public bool IsAdventureComplete(Adventure row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsAdventureComplete(row.RowId - 0x210000); + } + /// <inheritdoc/> public bool IsAetherCurrentUnlocked(AetherCurrent row) { @@ -527,6 +536,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (rowRef.TryGetValue<ActionSheet>(out var actionRow)) return this.IsActionUnlocked(actionRow); + if (rowRef.TryGetValue<Adventure>(out var adventureRow)) + return this.IsAdventureComplete(adventureRow); + if (rowRef.TryGetValue<AetherCurrent>(out var aetherCurrentRow)) return this.IsAetherCurrentUnlocked(aetherCurrentRow); @@ -710,10 +722,11 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return; Log.Verbose("Checking for new unlocks..."); - + // Do not check for Achievements or Titles here! this.UpdateUnlocksForSheet<ActionSheet>(); + this.UpdateUnlocksForSheet<Adventure>(); this.UpdateUnlocksForSheet<AetherCurrent>(); this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>(); this.UpdateUnlocksForSheet<AozAction>(); @@ -760,7 +773,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // For some other day: // - FishingSpot // - Spearfishing - // - Adventure (Sightseeing) // - MinerFolkloreTome // - BotanistFolkloreTome // - FishingFolkloreTome @@ -851,6 +863,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// <inheritdoc/> public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row); + /// <inheritdoc/> + public bool IsAdventureComplete(Adventure row) => this.unlockStateService.IsAdventureComplete(row); + /// <inheritdoc/> public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 4554aa318..4f92b2b3d 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -46,6 +46,13 @@ public interface IUnlockState : IDalamudService /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> bool IsActionUnlocked(Lumina.Excel.Sheets.Action row); + /// <summary> + /// Determines whether the specified Adventure is completed. + /// </summary> + /// <param name="row">The Adventure row to check.</param> + /// <returns><see langword="true"/> if completed; otherwise, <see langword="false"/>.</returns> + public bool IsAdventureComplete(Adventure row); + /// <summary> /// Determines whether the specified AetherCurrentCompFlgSet is unlocked. /// </summary> From 1779d2681ad5d800febbd4c63e7037fae9925da6 Mon Sep 17 00:00:00 2001 From: Haselnussbomber <mail@haselnussbomber.de> Date: Sat, 31 Jan 2026 22:16:07 +0100 Subject: [PATCH 470/477] Switch to CS in UnlockState --- Dalamud/Game/UnlockState/UnlockState.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 7e8f64adf..4b83e114a 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -33,9 +33,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState { private static readonly ModuleLog Log = new(nameof(UnlockState)); - [ServiceManager.ServiceDependency] - private readonly TargetSigScanner sigScanner = Service<TargetSigScanner>.Get(); - [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service<DataManager>.Get(); @@ -49,8 +46,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState private readonly RecipeData recipeData = Service<RecipeData>.Get(); private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = []; - private readonly Hook<SetAchievementCompletedDelegate> setAchievementCompletedHook; - private readonly Hook<SetTitleUnlockedDelegate> setTitleUnlockedHook; + private readonly Hook<CSAchievement.Delegates.SetAchievementCompleted> setAchievementCompletedHook; + private readonly Hook<TitleList.Delegates.SetTitleUnlocked> setTitleUnlockedHook; [ServiceManager.ServiceConstructor] private UnlockState() @@ -59,22 +56,18 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.clientState.Logout += this.OnLogout; this.gameGui.AgentUpdate += this.OnAgentUpdate; - this.setAchievementCompletedHook = Hook<SetAchievementCompletedDelegate>.FromAddress( - this.sigScanner.ScanText("81 FA ?? ?? ?? ?? 0F 87 ?? ?? ?? ?? 53"), + this.setAchievementCompletedHook = Hook<CSAchievement.Delegates.SetAchievementCompleted>.FromAddress( + (nint)CSAchievement.MemberFunctionPointers.SetAchievementCompleted, this.SetAchievementCompletedDetour); - this.setTitleUnlockedHook = Hook<SetTitleUnlockedDelegate>.FromAddress( - this.sigScanner.ScanText("B8 ?? ?? ?? ?? 66 3B D0 73 ?? 44 0F B7 C2 49 C1 E8 ?? 4C 03 C1 0F B7 C2 83 E0 ?? 41 0F B6 48 ?? 0F AB C1"), + this.setTitleUnlockedHook = Hook<TitleList.Delegates.SetTitleUnlocked>.FromAddress( + (nint)TitleList.MemberFunctionPointers.SetTitleUnlocked, this.SetTitleUnlockedDetour); this.setAchievementCompletedHook.Enable(); this.setTitleUnlockedHook.Enable(); } - private delegate void SetAchievementCompletedDelegate(CSAchievement* thisPtr, uint id); - - private delegate void SetTitleUnlockedDelegate(TitleList* thisPtr, ushort id); - /// <inheritdoc/> public event IUnlockState.UnlockDelegate Unlock; From f01971a7d7667ccb10eb7448077a1144cc6a04b0 Mon Sep 17 00:00:00 2001 From: AtmoOmen <atmoomen2004@outlook.com> Date: Thu, 12 Feb 2026 16:54:55 +0800 Subject: [PATCH 471/477] fix Addon/AgentLifyCycle unreg (cherry picked from commit 29e1715ff589b9dddf5a747b8655ea382e08cf58) --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 6 ++++++ Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs | 5 +++++ Dalamud/Game/Agent/AgentLifecycle.cs | 6 ++++++ Dalamud/Game/Agent/AgentLifecycleEventListener.cs | 5 +++++ 4 files changed, 22 insertions(+) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 6520ee4cf..2e5439ed0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -94,6 +94,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// <param name="listener">The listener to unregister.</param> internal void UnregisterListener(AddonLifecycleEventListener listener) { + listener.IsRequestedToClear = true; + if (this.isInvokingListeners) { this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); @@ -122,6 +124,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { foreach (var listener in globalListeners) { + if (listener.IsRequestedToClear) continue; + try { listener.FunctionDelegate.Invoke(eventType, args); @@ -138,6 +142,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { foreach (var listener in addonListener) { + if (listener.IsRequestedToClear) continue; + try { listener.FunctionDelegate.Invoke(eventType, args); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs index fc82e0582..38c081e65 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs @@ -35,4 +35,9 @@ internal class AddonLifecycleEventListener /// Gets the delegate this listener invokes. /// </summary> public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; } + + /// <summary> + /// Gets or sets if the listener is requested to be cleared. + /// </summary> + internal bool IsRequestedToClear { get; set; } } diff --git a/Dalamud/Game/Agent/AgentLifecycle.cs b/Dalamud/Game/Agent/AgentLifecycle.cs index 45f0dec5c..1c895f9da 100644 --- a/Dalamud/Game/Agent/AgentLifecycle.cs +++ b/Dalamud/Game/Agent/AgentLifecycle.cs @@ -107,6 +107,8 @@ internal unsafe class AgentLifecycle : IInternalDisposableService /// <param name="listener">The listener to unregister.</param> internal void UnregisterListener(AgentLifecycleEventListener listener) { + listener.IsRequestedToClear = true; + if (this.isInvokingListeners) { this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); @@ -135,6 +137,8 @@ internal unsafe class AgentLifecycle : IInternalDisposableService { foreach (var listener in globalListeners) { + if (listener.IsRequestedToClear) continue; + try { listener.FunctionDelegate.Invoke(eventType, args); @@ -151,6 +155,8 @@ internal unsafe class AgentLifecycle : IInternalDisposableService { foreach (var listener in agentListener) { + if (listener.IsRequestedToClear) continue; + try { listener.FunctionDelegate.Invoke(eventType, args); diff --git a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs index 91f8aa3d3..592c126ba 100644 --- a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs +++ b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs @@ -35,4 +35,9 @@ public class AgentLifecycleEventListener /// Gets the delegate this listener invokes. /// </summary> public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; } + + /// <summary> + /// Gets or sets if the listener is requested to be cleared. + /// </summary> + internal bool IsRequestedToClear { get; set; } } From fd85a8d3bc64e6a638101d455b4b2e0089f0a43f Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:43:59 +0100 Subject: [PATCH 472/477] Enhance HookVerifier to check marshaled types --- .../Hooking/Internal/Verification/HookVerifier.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs index ebe6851ce..349d8f170 100644 --- a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -67,6 +67,7 @@ internal static class HookVerifier } var passedType = typeof(T); + var isAssemblyMarshaled = passedType.Assembly.GetCustomAttribute<DisableRuntimeMarshallingAttribute>() is not null; // Directly compare delegates if (passedType == entry.TargetDelegateType) @@ -78,7 +79,7 @@ internal static class HookVerifier var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; // Compare Return Type - var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType); + var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType, isAssemblyMarshaled); // Compare Parameter Count var passedParams = passedInvoke.GetParameters(); @@ -93,7 +94,7 @@ internal static class HookVerifier // Compare Parameter Types for (var i = 0; i < passedParams.Length; i++) { - if (!CheckParam(passedParams[i].ParameterType, enforcedParams[i].ParameterType)) + if (!CheckParam(passedParams[i].ParameterType, enforcedParams[i].ParameterType, isAssemblyMarshaled)) { mismatch = true; break; @@ -107,18 +108,18 @@ internal static class HookVerifier } } - private static bool CheckParam(Type paramLeft, Type paramRight) + private static bool CheckParam(Type paramLeft, Type paramRight, bool isMarshaled) { var sameType = paramLeft == paramRight; - return sameType || SizeOf(paramLeft) == SizeOf(paramRight); + return sameType || SizeOf(paramLeft, isMarshaled) == SizeOf(paramRight, false); } - private static int SizeOf(Type type) + private static int SizeOf(Type type, bool isMarshaled) { return type switch { - _ when type == typeof(sbyte) || type == typeof(byte) || type == typeof(bool) => 1, + _ when type == typeof(sbyte) || type == typeof(byte) || (type == typeof(bool) && !isMarshaled) => 1, _ when type == typeof(char) || type == typeof(short) || type == typeof(ushort) || type == typeof(Half) => 2, - _ when type == typeof(int) || type == typeof(uint) || type == typeof(float) => 4, + _ when type == typeof(int) || type == typeof(uint) || type == typeof(float) || (type == typeof(bool) && isMarshaled) => 4, _ when type == typeof(long) || type == typeof(ulong) || type == typeof(double) || type.IsPointer || type.IsFunctionPointer || type.IsUnmanagedFunctionPointer || (type.Name == "Pointer`1" && type.Namespace.AsSpan().SequenceEqual(ClientStructsInteropNamespacePrefix)) || type == typeof(CStringPointer) => 8, _ when type.Name.StartsWith("FixedSizeArray") => SizeOf(type.GetGenericArguments()[0]) * int.Parse(type.Name[14..type.Name.IndexOf('`')]), _ when type.GetCustomAttribute<InlineArrayAttribute>() is { Length: var length } => SizeOf(type.GetGenericArguments()[0]) * length, From 7752b0f91825ed134689a179f2c180a294357399 Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:49:11 +0100 Subject: [PATCH 473/477] Add SendPacket delegate to HookVerifier --- Dalamud/Hooking/Internal/Verification/HookVerifier.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs index 349d8f170..e02fd9db6 100644 --- a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -6,6 +6,8 @@ using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Logging.Internal; +using FFXIVClientStructs.FFXIV.Application.Network; + using InteropGenerator.Runtime; namespace Dalamud.Hooking.Internal.Verification; @@ -25,7 +27,12 @@ internal static class HookVerifier "ActorControlSelf", "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", typeof(ActorControlSelfDelegate), // TODO: change this to CS delegate - "Signature changed in Patch 7.4") // 7.4 (new parameters) + "Signature changed in Patch 7.4"), // 7.4 (new parameters) + new( + "SendPacket", + ZoneClient.Addresses.SendPacket.String, + typeof(ZoneClient.Delegates.SendPacket), + "Force marshaling context") // If people hook with 4 byte return this locks people out from logging in ]; private static readonly string ClientStructsInteropNamespacePrefix = string.Join(".", nameof(FFXIVClientStructs), nameof(FFXIVClientStructs.Interop)); From 87a0c69020b9a5a262b9450f0953a935ddf3c75f Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:58:51 +0100 Subject: [PATCH 474/477] Fix missing args --- Dalamud/Hooking/Internal/Verification/HookVerifier.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs index e02fd9db6..bad7dc470 100644 --- a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -128,10 +128,10 @@ internal static class HookVerifier _ when type == typeof(char) || type == typeof(short) || type == typeof(ushort) || type == typeof(Half) => 2, _ when type == typeof(int) || type == typeof(uint) || type == typeof(float) || (type == typeof(bool) && isMarshaled) => 4, _ when type == typeof(long) || type == typeof(ulong) || type == typeof(double) || type.IsPointer || type.IsFunctionPointer || type.IsUnmanagedFunctionPointer || (type.Name == "Pointer`1" && type.Namespace.AsSpan().SequenceEqual(ClientStructsInteropNamespacePrefix)) || type == typeof(CStringPointer) => 8, - _ when type.Name.StartsWith("FixedSizeArray") => SizeOf(type.GetGenericArguments()[0]) * int.Parse(type.Name[14..type.Name.IndexOf('`')]), - _ when type.GetCustomAttribute<InlineArrayAttribute>() is { Length: var length } => SizeOf(type.GetGenericArguments()[0]) * length, + _ when type.Name.StartsWith("FixedSizeArray") => SizeOf(type.GetGenericArguments()[0], isMarshaled) * int.Parse(type.Name[14..type.Name.IndexOf('`')]), + _ when type.GetCustomAttribute<InlineArrayAttribute>() is { Length: var length } => SizeOf(type.GetGenericArguments()[0], isMarshaled) * length, _ when IsStruct(type) && !type.IsGenericType && (type.StructLayoutAttribute?.Value ?? LayoutKind.Sequential) != LayoutKind.Sequential => type.StructLayoutAttribute?.Size ?? (int?)typeof(Unsafe).GetMethod("SizeOf")?.MakeGenericMethod(type).Invoke(null, null) ?? 0, - _ when type.IsEnum => SizeOf(Enum.GetUnderlyingType(type)), + _ when type.IsEnum => SizeOf(Enum.GetUnderlyingType(type), isMarshaled), _ when type.IsGenericType => Marshal.SizeOf(Activator.CreateInstance(type)!), _ => GetSizeOf(type), }; From eb8e431267b98cf5a8deb635d75d178e1758164a Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:22:29 +0100 Subject: [PATCH 475/477] Invert condition for assembly marshaling check --- Dalamud/Hooking/Internal/Verification/HookVerifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs index bad7dc470..98568a567 100644 --- a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -74,7 +74,7 @@ internal static class HookVerifier } var passedType = typeof(T); - var isAssemblyMarshaled = passedType.Assembly.GetCustomAttribute<DisableRuntimeMarshallingAttribute>() is not null; + var isAssemblyMarshaled = passedType.Assembly.GetCustomAttribute<DisableRuntimeMarshallingAttribute>() is null; // Directly compare delegates if (passedType == entry.TargetDelegateType) From 9c73cbe596c8479301f0dc19fcc564ba57ba6750 Mon Sep 17 00:00:00 2001 From: goaaats <goatsdev@protonmail.com> Date: Sun, 15 Feb 2026 17:42:21 +0100 Subject: [PATCH 476/477] Don't throw in HookVerifier if user does not have devPlugins, until api15 --- .../Internal/Verification/HookVerifier.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs index 98568a567..721798a40 100644 --- a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Logging.Internal; @@ -65,6 +66,13 @@ internal static class HookVerifier /// <exception cref="HookVerificationException">Exception thrown when we think the hook is not correctly declared.</exception> public static void Verify<T>(IntPtr address) where T : Delegate { + // API15 TODO: Always throw + var config = Service<DalamudConfiguration>.GetNullable(); + if (config != null && config.DevPluginLoadLocations.Count == 0) + { + return; + } + var entry = ToVerify.FirstOrDefault(x => x.Address == address); // Nothing to verify for this hook? @@ -121,7 +129,7 @@ internal static class HookVerifier return sameType || SizeOf(paramLeft, isMarshaled) == SizeOf(paramRight, false); } - private static int SizeOf(Type type, bool isMarshaled) + private static int SizeOf(Type type, bool isMarshaled) { return type switch { _ when type == typeof(sbyte) || type == typeof(byte) || (type == typeof(bool) && !isMarshaled) => 1, @@ -137,19 +145,19 @@ internal static class HookVerifier }; } - private static int GetSizeOf(Type type) + private static int GetSizeOf(Type type) { - try + try { return Marshal.SizeOf(Activator.CreateInstance(type)!); - } - catch + } + catch { return 0; } } - private static bool IsStruct(Type type) + private static bool IsStruct(Type type) { return type != typeof(decimal) && type is { IsValueType: true, IsPrimitive: false, IsEnum: false }; } From 2deeacd4434a67acbb49eb1cf8db301ea283472e Mon Sep 17 00:00:00 2001 From: goaaats <goatsdev@protonmail.com> Date: Sun, 15 Feb 2026 18:12:32 +0100 Subject: [PATCH 477/477] build: 14.0.2.2 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index adaf876ee..5a89f08ae 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ <PropertyGroup Label="Feature"> <Description>XIV Launcher addon framework</Description> - <DalamudVersion>14.0.2.1</DalamudVersion> + <DalamudVersion>14.0.2.2</DalamudVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <Version>$(DalamudVersion)</Version> <FileVersion>$(DalamudVersion)</FileVersion>