From a1d2e275a78680fa1fc33a693f453189012ab4c5 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 3 Jan 2026 20:54:24 +0100 Subject: [PATCH 01/15] - 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 02/15] - 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 03/15] 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 04/15] 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 05/15] 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 06/15] - 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 d0caf98eb3d0333ee7fb3226fefca77fecc052f0 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 4 Jan 2026 21:40:31 -0800 Subject: [PATCH 07/15] 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 f635673ce91e33412a3ed1bcc477567906787940 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 9 Jan 2026 12:51:08 -0800 Subject: [PATCH 08/15] 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 09/15] 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 fab7eef244e17a66c57caf1131709f501cf3c7f7 Mon Sep 17 00:00:00 2001 From: Infi Date: Sat, 10 Jan 2026 14:25:22 +0100 Subject: [PATCH 10/15] 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 11/15] 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 12/15] 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 13/15] 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 14/15] 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 15/15] 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 { ///