From c76cbbd5183e745368e5f7cccfa9e0d9d44d6e11 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 May 2023 10:33:53 +0200 Subject: [PATCH 01/81] dragdrop --- Dalamud.CorePlugin/PluginImpl.cs | 3 +- Dalamud/Dalamud.csproj | 1 + Dalamud/Interface/DragDrop/DragDropInterop.cs | 74 ++++++++++++ Dalamud/Interface/DragDrop/DragDropManager.cs | 106 ++++++++++++++++++ Dalamud/Interface/DragDrop/DragDropTarget.cs | 23 ++++ .../Interface/DragDrop/IDragDropManager.cs | 43 +++++++ Dalamud/Interface/UiBuilder.cs | 9 ++ 7 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Interface/DragDrop/DragDropInterop.cs create mode 100644 Dalamud/Interface/DragDrop/DragDropManager.cs create mode 100644 Dalamud/Interface/DragDrop/DragDropTarget.cs create mode 100644 Dalamud/Interface/DragDrop/IDragDropManager.cs diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index d352ad2c8..206c578c2 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -1,12 +1,13 @@ using System; using System.IO; - +using System.Windows.Forms; using Dalamud.Configuration.Internal; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; +using PInvoke; namespace Dalamud.CorePlugin { diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 61730b5ca..0f259ee31 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -82,6 +82,7 @@ + diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs new file mode 100644 index 000000000..d25400834 --- /dev/null +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows.Forms; + +namespace Dalamud.Interface.DragDrop; + +internal partial class DragDropManager +{ + private static class DragDropInterop + { + [Flags] + public enum ModifierKeys + { + MK_NONE = 0x00, + MK_LBUTTON = 0x01, + MK_RBUTTON = 0x02, + MK_SHIFT = 0x04, + MK_CONTROL = 0x08, + MK_MBUTTON = 0x10, + MK_ALT = 0x20, + } + + public enum ClipboardFormat + { + CF_TEXT = 1, + CF_BITMAP = 2, + CF_DIB = 3, + CF_UNICODETEXT = 13, + CF_HDROP = 15, + } + + [Flags] + public enum DVAspect + { + DVASPECT_CONTENT = 0x01, + DVASPECT_THUMBNAIL = 0x02, + DVASPECT_ICON = 0x04, + DVASPECT_DOCPRINT = 0x08, + } + + [Flags] + public enum TYMED + { + TYMED_NULL = 0x00, + TYMED_HGLOBAL = 0x01, + TYMED_FILE = 0x02, + TYMED_ISTREAM = 0x04, + TYMED_ISTORAGE = 0x08, + TYMED_GDI = 0x10, + TYMED_MFPICT = 0x20, + TYMED_ENHMF = 0x40, + } + + [Flags] + public enum DropEffects : uint + { + None = 0x00_0000_00, + Copy = 0x00_0000_01, + Move = 0x00_0000_02, + Link = 0x00_0000_04, + Scroll = 0x80_0000_00, + } + + [DllImport("ole32.dll")] + public static extern int RegisterDragDrop(nint hwnd, IDropTarget pDropTarget); + + [DllImport("ole32.dll")] + public static extern int RevokeDragDrop(nint hwnd); + + [DllImport("shell32.dll")] + public static extern int DragQueryFile(IntPtr hDrop, uint iFile, StringBuilder lpszFile, int cch); + } +} diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs new file mode 100644 index 000000000..ab838dc45 --- /dev/null +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -0,0 +1,106 @@ +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Serilog; + +namespace Dalamud.Interface.DragDrop; + +internal partial class DragDropManager : IDisposable, IDragDropManager +{ + private readonly UiBuilder uiBuilder; + + public DragDropManager(UiBuilder uiBuilder) + => this.uiBuilder = uiBuilder; + + public void Enable() + { + if (this.ServiceAvailable) + { + return; + } + + try + { + var ret2 = DragDropInterop.RegisterDragDrop(this.uiBuilder.WindowHandlePtr, this); + Marshal.ThrowExceptionForHR(ret2); + this.ServiceAvailable = true; + } + catch (Exception ex) + { + Log.Error($"Could not create windows drag and drop utility:\n{ex}"); + } + } + + public void Dispose() + { + if (!this.ServiceAvailable) + { + return; + } + + + } + + public bool ServiceAvailable { get; internal set; } + + public bool IsDragging { get; private set; } + + public IReadOnlyList Files { get; private set; } = Array.Empty(); + + public IReadOnlySet Extensions { get; private set; } = new HashSet(); + + public IReadOnlyList Directories { get; private set; } = Array.Empty(); + + /// + public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder) + { + if (!this.IsDragging && !this.IsDropping()) return; + if (!validityCheck(this) || !ImGui.BeginDragDropSource(ImGuiDragDropFlags.SourceExtern)) return; + + ImGui.SetDragDropPayload(label, nint.Zero, 0); + if (this.CheckTooltipFrame(out var frame) && tooltipBuilder(this)) + { + this.lastTooltipFrame = frame; + } + + ImGui.EndDragDropSource(); + } + + /// + public bool CreateImGuiTarget(string label, out IReadOnlyList files, out IReadOnlyList directories) + { + files = Array.Empty(); + directories = Array.Empty(); + if (!this.IsDragging || !ImGui.BeginDragDropTarget()) return false; + + unsafe + { + if (ImGui.AcceptDragDropPayload(label, ImGuiDragDropFlags.AcceptBeforeDelivery).NativePtr != null && this.IsDropping()) + { + this.lastDropFrame = -2; + files = this.Files; + directories = this.Directories; + return true; + } + } + + ImGui.EndDragDropTarget(); + return false; + } + + private int lastDropFrame = -2; + private int lastTooltipFrame = -1; + + private bool CheckTooltipFrame(out int frame) + { + frame = ImGui.GetFrameCount(); + return this.lastTooltipFrame < frame; + } + + private bool IsDropping() + { + var frame = ImGui.GetFrameCount(); + return this.lastDropFrame == frame || this.lastDropFrame == frame - 1; + } +} diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs new file mode 100644 index 000000000..cff97a987 --- /dev/null +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.VisualStudio.OLE.Interop; + +namespace Dalamud.Interface.DragDrop; + +internal partial class DragDropManager : IDropTarget +{ + public void DragEnter(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect) + { + } + + public void DragOver(uint grfKeyState, POINTL pt, ref uint pdwEffect) + { + } + + public void DragLeave() + { + } + + public void Drop(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect) + { + } +} diff --git a/Dalamud/Interface/DragDrop/IDragDropManager.cs b/Dalamud/Interface/DragDrop/IDragDropManager.cs new file mode 100644 index 000000000..2a0e7bdd9 --- /dev/null +++ b/Dalamud/Interface/DragDrop/IDragDropManager.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace Dalamud.Interface.DragDrop; + +/// +/// A service to handle external drag and drop from WinAPI. +/// +public interface IDragDropManager +{ + /// Gets a value indicating whether Drag and Drop functionality is available at all. + public bool ServiceAvailable { get; } + + /// Gets a value indicating whether anything is being dragged from an external application and over any of the games viewports. + public bool IsDragging { get; } + + /// Gets the list of files currently being dragged from an external application over any of the games viewports. + public IReadOnlyList Files { get; } + + /// Gets the set of file types by extension currently being dragged from an external application over any of the games viewports. + public IReadOnlySet Extensions { get; } + + /// Gets the list of directories currently being dragged from an external application over any of the games viewports. + public IReadOnlyList Directories { get; } + + /// Create an ImGui drag & drop source that is active only if anything is being dragged from an external source. + /// The label used for the drag & drop payload. + /// A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged. + public void CreateImGuiSource(string label, Func validityCheck) + => this.CreateImGuiSource(label, validityCheck, _ => false); + + /// Create an ImGui drag & drop source that is active only if anything is being dragged from an external source. + /// The label used for the drag & drop payload. + /// A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged. + /// Executes ImGui functions to build a tooltip. Should return true if it creates any tooltip and false otherwise. If multiple sources are active, only the first non-empty tooltip type drawn in a frame will be used. + public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder); + + /// Create an ImGui drag & drop target on the last ImGui object. + /// The label used for the drag & drop payload. + /// On success, contains the files dropped onto the target. + /// True if items were dropped onto the target this frame, false otherwise. + public bool CreateImGuiTarget(string label, out IReadOnlyList files); +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index da4f241ac..79541648b 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -7,6 +7,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; +using Dalamud.Interface.DragDrop; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; @@ -30,6 +31,7 @@ public sealed class UiBuilder : IDisposable private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); private readonly GameFontManager gameFontManager = Service.Get(); + private readonly DragDropManager dragDropManager; private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -47,6 +49,7 @@ public sealed class UiBuilder : IDisposable this.stopwatch = new Stopwatch(); this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); this.namespaceName = namespaceName; + this.dragDropManager = new DragDropManager(this); this.interfaceManager.Draw += this.OnDraw; this.interfaceManager.BuildFonts += this.OnBuildFonts; @@ -100,6 +103,11 @@ public sealed class UiBuilder : IDisposable /// public event Action HideUi; + /// + /// Gets the manager for external, WinAPI-based drag and drop functionality. + /// + public IDragDropManager DragDropManager => this.dragDropManager; + /// /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. /// @@ -397,6 +405,7 @@ public sealed class UiBuilder : IDisposable /// void IDisposable.Dispose() { + this.dragDropManager.Dispose(); this.interfaceManager.Draw -= this.OnDraw; this.interfaceManager.BuildFonts -= this.OnBuildFonts; this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; From 96bb94b9d5334bed5e53748ce2daacfebe515eda Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 May 2023 18:49:52 +0200 Subject: [PATCH 02/81] Improve Drag & Drop interface. --- Dalamud.CorePlugin/PluginImpl.cs | 3 +- Dalamud/Interface/DragDrop/DragDropInterop.cs | 7 +- Dalamud/Interface/DragDrop/DragDropManager.cs | 74 ++++-- Dalamud/Interface/DragDrop/DragDropTarget.cs | 215 ++++++++++++++++++ .../Interface/DragDrop/IDragDropManager.cs | 5 +- Dalamud/Interface/UiBuilder.cs | 1 + 6 files changed, 283 insertions(+), 22 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 206c578c2..d352ad2c8 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -1,13 +1,12 @@ using System; using System.IO; -using System.Windows.Forms; + using Dalamud.Configuration.Internal; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; -using PInvoke; namespace Dalamud.CorePlugin { diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs index d25400834..b3befac07 100644 --- a/Dalamud/Interface/DragDrop/DragDropInterop.cs +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -1,10 +1,15 @@ using System; using System.Runtime.InteropServices; using System.Text; -using System.Windows.Forms; +using Microsoft.VisualStudio.OLE.Interop; + +// ReSharper disable UnusedMember.Local +// ReSharper disable IdentifierTypo +// ReSharper disable InconsistentNaming namespace Dalamud.Interface.DragDrop; +/// Implements interop enums and function calls to interact with external drag and drop. internal partial class DragDropManager { private static class DragDropInterop diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index ab838dc45..c27149153 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -1,18 +1,47 @@ -using ImGuiNET; using System; using System.Collections.Generic; using System.Runtime.InteropServices; + +using ImGuiNET; using Serilog; namespace Dalamud.Interface.DragDrop; +/// +/// A manager that keeps state of external windows drag and drop events, +/// and can be used to create ImGui drag and drop sources and targets for those external events. +/// internal partial class DragDropManager : IDisposable, IDragDropManager { private readonly UiBuilder uiBuilder; + private int lastDropFrame = -2; + private int lastTooltipFrame = -1; + + /// Initializes a new instance of the class. + /// The parent instance. public DragDropManager(UiBuilder uiBuilder) => this.uiBuilder = uiBuilder; + /// Gets a value indicating whether external drag and drop is available at all. + public bool ServiceAvailable { get; private set; } + + /// Gets a value indicating whether a valid external drag and drop is currently active and hovering over any FFXIV-related viewport. + public bool IsDragging { get; private set; } + + /// Gets a value indicating whether there are any files or directories currently being dragged. + public bool HasPaths { get; private set; } + + /// Gets the list of file paths currently being dragged from an external application over any FFXIV-related viewport. + public IReadOnlyList Files { get; private set; } = Array.Empty(); + + /// Gets a set of all extensions available in the paths currently being dragged from an external application over any FFXIV-related viewport. + public IReadOnlySet Extensions { get; private set; } = new HashSet(); + + /// Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport. + public IReadOnlyList Directories { get; private set; } = Array.Empty(); + + /// Enable external drag and drop. public void Enable() { if (this.ServiceAvailable) @@ -32,31 +61,42 @@ internal partial class DragDropManager : IDisposable, IDragDropManager } } - public void Dispose() + /// Disable external drag and drop. + public void Disable() { if (!this.ServiceAvailable) { return; } + try + { + DragDropInterop.RevokeDragDrop(this.uiBuilder.WindowHandlePtr); + } + catch (Exception ex) + { + Log.Error($"Could not disable windows drag and drop utility:\n{ex}"); + } + this.ServiceAvailable = false; } - public bool ServiceAvailable { get; internal set; } - - public bool IsDragging { get; private set; } - - public IReadOnlyList Files { get; private set; } = Array.Empty(); - - public IReadOnlySet Extensions { get; private set; } = new HashSet(); - - public IReadOnlyList Directories { get; private set; } = Array.Empty(); + /// + public void Dispose() + => this.Disable(); /// public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder) { - if (!this.IsDragging && !this.IsDropping()) return; - if (!validityCheck(this) || !ImGui.BeginDragDropSource(ImGuiDragDropFlags.SourceExtern)) return; + if (!this.HasPaths && !this.IsDropping()) + { + return; + } + + if (!validityCheck(this) || !ImGui.BeginDragDropSource(ImGuiDragDropFlags.SourceExtern)) + { + return; + } ImGui.SetDragDropPayload(label, nint.Zero, 0); if (this.CheckTooltipFrame(out var frame) && tooltipBuilder(this)) @@ -72,7 +112,10 @@ internal partial class DragDropManager : IDisposable, IDragDropManager { files = Array.Empty(); directories = Array.Empty(); - if (!this.IsDragging || !ImGui.BeginDragDropTarget()) return false; + if (!this.IsDragging || !ImGui.BeginDragDropTarget()) + { + return false; + } unsafe { @@ -89,9 +132,6 @@ internal partial class DragDropManager : IDisposable, IDragDropManager return false; } - private int lastDropFrame = -2; - private int lastTooltipFrame = -1; - private bool CheckTooltipFrame(out int frame) { frame = ImGui.GetFrameCount(); diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index cff97a987..79a20cff0 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -1,23 +1,238 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +using Dalamud.Utility; +using ImGuiNET; using Microsoft.VisualStudio.OLE.Interop; +using Serilog; namespace Dalamud.Interface.DragDrop; +/// Implements the IDropTarget interface to interact with external drag and dropping. internal partial class DragDropManager : IDropTarget { + /// Create the drag and drop formats we accept. + private static readonly FORMATETC[] FormatEtc = + { + new() + { + cfFormat = (ushort)DragDropInterop.ClipboardFormat.CF_HDROP, + ptd = nint.Zero, + dwAspect = (uint)DragDropInterop.DVAspect.DVASPECT_CONTENT, + lindex = -1, + tymed = (uint)DragDropInterop.TYMED.TYMED_HGLOBAL, + }, + }; + + /// + /// Invoked whenever a drag and drop process drags files into any FFXIV-related viewport. + /// + /// The drag and drop data. + /// The mouse button used to drag as well as key modifiers. + /// The global cursor position. + /// Effects that can be used with this drag and drop process. public void DragEnter(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect) { + this.IsDragging = true; + UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, true); + + if (pDataObj.QueryGetData(FormatEtc) != 0) + { + pdwEffect = 0; + } + else + { + pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; + (this.Files, this.Directories) = this.GetPaths(pDataObj); + this.HasPaths = this.Files.Count + this.Directories.Count > 0; + this.Extensions = this.Files.Select(Path.GetExtension).Where(p => !p.IsNullOrEmpty()).Distinct().ToHashSet(); + } } + /// Invoked every windows update-frame as long as the drag and drop process keeps hovering over an FFXIV-related viewport. + /// The mouse button used to drag as well as key modifiers. + /// The global cursor position. + /// Effects that can be used with this drag and drop process. + /// Can be invoked more often than once a XIV frame, can also be less often (?). public void DragOver(uint grfKeyState, POINTL pt, ref uint pdwEffect) { + UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false); + pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; } + /// Invoked whenever a drag and drop process that hovered over any FFXIV-related viewport leaves all FFXIV-related viewports. public void DragLeave() { + this.IsDragging = false; + this.Files = Array.Empty(); + this.Directories = Array.Empty(); + this.Extensions = new HashSet(); } + /// Invoked whenever a drag process ends by dropping over any FFXIV-related viewport. + /// The drag and drop data. + /// The mouse button used to drag as well as key modifiers. + /// The global cursor position. + /// Effects that can be used with this drag and drop process. public void Drop(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect) { + MouseDrop((DragDropInterop.ModifierKeys)grfKeyState); + this.lastDropFrame = ImGui.GetFrameCount(); + this.IsDragging = false; + if (this.Files.Count > 0 || this.Directories.Count > 0) + { + pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; + } + else + { + pdwEffect = 0; + } + } + + private static void UpdateIo(DragDropInterop.ModifierKeys keys, bool entering) + { + var io = ImGui.GetIO(); + void UpdateMouse(int mouseIdx) + { + if (entering) + { + io.MouseDownDuration[mouseIdx] = 1f; + } + + io.MouseDown[mouseIdx] = true; + io.AddMouseButtonEvent(mouseIdx, true); + } + + if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_LBUTTON)) + { + UpdateMouse(0); + } + + if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_RBUTTON)) + { + UpdateMouse(1); + } + + if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_MBUTTON)) + { + UpdateMouse(2); + } + + if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_CONTROL)) + { + io.KeyCtrl = true; + io.AddKeyEvent(ImGuiKey.LeftCtrl, true); + } + else + { + io.KeyCtrl = false; + io.AddKeyEvent(ImGuiKey.LeftCtrl, false); + } + + if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_ALT)) + { + io.KeyAlt = true; + io.AddKeyEvent(ImGuiKey.LeftAlt, true); + } + else + { + io.KeyAlt = false; + io.AddKeyEvent(ImGuiKey.LeftAlt, false); + } + + if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_SHIFT)) + { + io.KeyShift = true; + io.AddKeyEvent(ImGuiKey.LeftShift, true); + } + else + { + io.KeyShift = false; + io.AddKeyEvent(ImGuiKey.LeftShift, false); + } + } + + private static void MouseDrop(DragDropInterop.ModifierKeys keys) + { + var io = ImGui.GetIO(); + void UpdateMouse(int mouseIdx) + { + io.AddMouseButtonEvent(mouseIdx, false); + io.MouseDown[mouseIdx] = false; + } + + if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_LBUTTON)) + { + UpdateMouse(0); + } + + if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_RBUTTON)) + { + UpdateMouse(1); + } + + if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_MBUTTON)) + { + UpdateMouse(2); + } + } + + private (string[] Files, string[] Directories) GetPaths(IDataObject data) + { + if (!this.IsDragging) + { + return (Array.Empty(), Array.Empty()); + } + + try + { + var stgMedium = new STGMEDIUM[] + { + default, + }; + data.GetData(FormatEtc, stgMedium); + var numFiles = DragDropInterop.DragQueryFile(stgMedium[0].unionmember, uint.MaxValue, new StringBuilder(), 0); + var files = new string[numFiles]; + var sb = new StringBuilder(1024); + var directoryCount = 0; + var fileCount = 0; + for (var i = 0u; i < numFiles; ++i) + { + sb.Clear(); + var ret = DragDropInterop.DragQueryFile(stgMedium[0].unionmember, i, sb, sb.Capacity); + if (ret >= sb.Capacity) + { + sb.Capacity = ret + 1; + ret = DragDropInterop.DragQueryFile(stgMedium[0].unionmember, i, sb, sb.Capacity); + } + + if (ret > 0 && ret < sb.Capacity) + { + var s = sb.ToString(); + if (Directory.Exists(s)) + { + files[^(++directoryCount)] = s; + } + else + { + files[fileCount++] = s; + } + } + } + + var fileArray = fileCount > 0 ? files.Take(fileCount).ToArray() : Array.Empty(); + var directoryArray = directoryCount > 0 ? files.TakeLast(directoryCount).Reverse().ToArray() : Array.Empty(); + + return (fileArray, directoryArray); + } + catch (Exception ex) + { + Log.Error($"Error obtaining data from drag & drop:\n{ex}"); + } + + return (Array.Empty(), Array.Empty()); } } diff --git a/Dalamud/Interface/DragDrop/IDragDropManager.cs b/Dalamud/Interface/DragDrop/IDragDropManager.cs index 2a0e7bdd9..736c8af24 100644 --- a/Dalamud/Interface/DragDrop/IDragDropManager.cs +++ b/Dalamud/Interface/DragDrop/IDragDropManager.cs @@ -37,7 +37,8 @@ public interface IDragDropManager /// Create an ImGui drag & drop target on the last ImGui object. /// The label used for the drag & drop payload. - /// On success, contains the files dropped onto the target. + /// On success, contains the list of file paths dropped onto the target. + /// On success, contains the list of directory paths dropped onto the target. /// True if items were dropped onto the target this frame, false otherwise. - public bool CreateImGuiTarget(string label, out IReadOnlyList files); + public bool CreateImGuiTarget(string label, out IReadOnlyList files, out IReadOnlyList directories); } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 79541648b..a421d17ba 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -50,6 +50,7 @@ public sealed class UiBuilder : IDisposable this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); this.namespaceName = namespaceName; this.dragDropManager = new DragDropManager(this); + this.dragDropManager.Enable(); this.interfaceManager.Draw += this.OnDraw; this.interfaceManager.BuildFonts += this.OnBuildFonts; From 0c53741b1de2bebfd3b928251f17148711d446d8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 May 2023 21:16:52 +0200 Subject: [PATCH 03/81] Remove OLE interop dependency. --- Dalamud/Dalamud.cs | 4 ++ Dalamud/Dalamud.csproj | 1 - Dalamud/Interface/DragDrop/DragDropInterop.cs | 27 ++++++++++- Dalamud/Interface/DragDrop/DragDropManager.cs | 30 +++++++----- Dalamud/Interface/DragDrop/DragDropTarget.cs | 48 +++++++++++-------- Dalamud/Interface/UiBuilder.cs | 5 +- 6 files changed, 76 insertions(+), 39 deletions(-) diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 73914d2a7..142f653ef 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.Gui.Internal; +using Dalamud.Interface.DragDrop; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; @@ -135,6 +136,9 @@ internal sealed class Dalamud : IServiceType // will not receive any windows messages Service.GetNullable()?.Dispose(); + // this must be done before unloading interface manager, since it relies on the window handle members. + Service.GetNullable()?.Dispose(); + // this must be done before unloading plugins, or it can cause a race condition // due to rendering happening on another thread, where a plugin might receive // a render call after it has been disposed, which can crash if it attempts to diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 0f259ee31..61730b5ca 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -82,7 +82,6 @@ - diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs index b3befac07..4a14d5e37 100644 --- a/Dalamud/Interface/DragDrop/DragDropInterop.cs +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -1,9 +1,9 @@ using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; using System.Text; -using Microsoft.VisualStudio.OLE.Interop; - // ReSharper disable UnusedMember.Local // ReSharper disable IdentifierTypo // ReSharper disable InconsistentNaming @@ -12,6 +12,29 @@ namespace Dalamud.Interface.DragDrop; /// Implements interop enums and function calls to interact with external drag and drop. internal partial class DragDropManager { + internal struct POINTL + { + [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] + public int x; + [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] + public int y; + } + + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("00000122-0000-0000-C000-000000000046")] + [ComImport] + public interface IDropTarget + { + [MethodImpl(MethodImplOptions.InternalCall)] + void DragEnter([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect); + [MethodImpl(MethodImplOptions.InternalCall)] + void DragOver([ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect); + [MethodImpl(MethodImplOptions.InternalCall)] + void DragLeave(); + [MethodImpl(MethodImplOptions.InternalCall)] + void Drop([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect); + } + private static class DragDropInterop { [Flags] diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index c27149153..2b1f79ed1 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; - +using System.Threading.Tasks; +using Dalamud.Interface.Internal; using ImGuiNET; using Serilog; @@ -11,17 +12,22 @@ namespace Dalamud.Interface.DragDrop; /// A manager that keeps state of external windows drag and drop events, /// and can be used to create ImGui drag and drop sources and targets for those external events. /// -internal partial class DragDropManager : IDisposable, IDragDropManager +[ServiceManager.EarlyLoadedService] +internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType { - private readonly UiBuilder uiBuilder; - + private InterfaceManager? interfaceManager; private int lastDropFrame = -2; private int lastTooltipFrame = -1; - /// Initializes a new instance of the class. - /// The parent instance. - public DragDropManager(UiBuilder uiBuilder) - => this.uiBuilder = uiBuilder; + [ServiceManager.ServiceConstructor] + private DragDropManager() + { + Service.GetAsync().ContinueWith(task => + { + this.interfaceManager = task.Result.Manager; + this.Enable(); + }); + } /// Gets a value indicating whether external drag and drop is available at all. public bool ServiceAvailable { get; private set; } @@ -44,14 +50,15 @@ internal partial class DragDropManager : IDisposable, IDragDropManager /// Enable external drag and drop. public void Enable() { - if (this.ServiceAvailable) + if (this.ServiceAvailable || this.interfaceManager == null) { return; } try { - var ret2 = DragDropInterop.RegisterDragDrop(this.uiBuilder.WindowHandlePtr, this); + var ret2 = DragDropInterop.RegisterDragDrop(this.interfaceManager.WindowHandlePtr, this); + Log.Information($"[DragDrop] Registered window {this.interfaceManager.WindowHandlePtr} for external drag and drop operations. ({ret2})"); Marshal.ThrowExceptionForHR(ret2); this.ServiceAvailable = true; } @@ -71,7 +78,8 @@ internal partial class DragDropManager : IDisposable, IDragDropManager try { - DragDropInterop.RevokeDragDrop(this.uiBuilder.WindowHandlePtr); + DragDropInterop.RevokeDragDrop(this.interfaceManager!.WindowHandlePtr); + Log.Information($"[DragDrop] Disabled external drag and drop operations for window {this.interfaceManager.WindowHandlePtr}."); } catch (Exception ex) { diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index 79a20cff0..a592991dd 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -2,30 +2,30 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using System.Text; using Dalamud.Utility; using ImGuiNET; -using Microsoft.VisualStudio.OLE.Interop; using Serilog; namespace Dalamud.Interface.DragDrop; /// Implements the IDropTarget interface to interact with external drag and dropping. -internal partial class DragDropManager : IDropTarget +internal partial class DragDropManager : DragDropManager.IDropTarget { + private int lastUpdateFrame = -1; + /// Create the drag and drop formats we accept. - private static readonly FORMATETC[] FormatEtc = - { + private static FORMATETC FormatEtc = new() { - cfFormat = (ushort)DragDropInterop.ClipboardFormat.CF_HDROP, + cfFormat = (short)DragDropInterop.ClipboardFormat.CF_HDROP, ptd = nint.Zero, - dwAspect = (uint)DragDropInterop.DVAspect.DVASPECT_CONTENT, + dwAspect = DVASPECT.DVASPECT_CONTENT, lindex = -1, - tymed = (uint)DragDropInterop.TYMED.TYMED_HGLOBAL, - }, - }; + tymed = TYMED.TYMED_HGLOBAL, + }; /// /// Invoked whenever a drag and drop process drags files into any FFXIV-related viewport. @@ -39,7 +39,7 @@ internal partial class DragDropManager : IDropTarget this.IsDragging = true; UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, true); - if (pDataObj.QueryGetData(FormatEtc) != 0) + if (pDataObj.QueryGetData(ref FormatEtc) != 0) { pdwEffect = 0; } @@ -50,17 +50,24 @@ internal partial class DragDropManager : IDropTarget this.HasPaths = this.Files.Count + this.Directories.Count > 0; this.Extensions = this.Files.Select(Path.GetExtension).Where(p => !p.IsNullOrEmpty()).Distinct().ToHashSet(); } + Log.Debug("[DragDrop] Entering external Drag and Drop with {KeyState} at {PtX}, {PtY} and with {N} files.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y, this.Files.Count + this.Directories.Count); } /// Invoked every windows update-frame as long as the drag and drop process keeps hovering over an FFXIV-related viewport. /// The mouse button used to drag as well as key modifiers. /// The global cursor position. /// Effects that can be used with this drag and drop process. - /// Can be invoked more often than once a XIV frame, can also be less often (?). + /// Can be invoked more often than once a XIV frame, so we are keeping track of frames to skip unnecessary updates. public void DragOver(uint grfKeyState, POINTL pt, ref uint pdwEffect) { - UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false); - pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; + var frame = ImGui.GetFrameCount(); + if (frame != this.lastUpdateFrame) + { + this.lastUpdateFrame = frame; + UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false); + pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; + Log.Verbose("[DragDrop] External Drag and Drop with {KeyState} at {PtX}, {PtY}.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y); + } } /// Invoked whenever a drag and drop process that hovered over any FFXIV-related viewport leaves all FFXIV-related viewports. @@ -70,6 +77,7 @@ internal partial class DragDropManager : IDropTarget this.Files = Array.Empty(); this.Directories = Array.Empty(); this.Extensions = new HashSet(); + Log.Debug("[DragDrop] Leaving external Drag and Drop."); } /// Invoked whenever a drag process ends by dropping over any FFXIV-related viewport. @@ -90,6 +98,8 @@ internal partial class DragDropManager : IDropTarget { pdwEffect = 0; } + + Log.Debug("[DragDrop] Dropping {N} files with {KeyState} at {PtX}, {PtY}.", this.Files.Count + this.Directories.Count, (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y); } private static void UpdateIo(DragDropInterop.ModifierKeys keys, bool entering) @@ -189,12 +199,8 @@ internal partial class DragDropManager : IDropTarget try { - var stgMedium = new STGMEDIUM[] - { - default, - }; - data.GetData(FormatEtc, stgMedium); - var numFiles = DragDropInterop.DragQueryFile(stgMedium[0].unionmember, uint.MaxValue, new StringBuilder(), 0); + data.GetData(ref FormatEtc, out var stgMedium); + var numFiles = DragDropInterop.DragQueryFile(stgMedium.unionmember, uint.MaxValue, new StringBuilder(), 0); var files = new string[numFiles]; var sb = new StringBuilder(1024); var directoryCount = 0; @@ -202,11 +208,11 @@ internal partial class DragDropManager : IDropTarget for (var i = 0u; i < numFiles; ++i) { sb.Clear(); - var ret = DragDropInterop.DragQueryFile(stgMedium[0].unionmember, i, sb, sb.Capacity); + var ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity); if (ret >= sb.Capacity) { sb.Capacity = ret + 1; - ret = DragDropInterop.DragQueryFile(stgMedium[0].unionmember, i, sb, sb.Capacity); + ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity); } if (ret > 0 && ret < sb.Capacity) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index a421d17ba..5bd317af5 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -31,7 +31,7 @@ public sealed class UiBuilder : IDisposable private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); private readonly GameFontManager gameFontManager = Service.Get(); - private readonly DragDropManager dragDropManager; + private readonly DragDropManager dragDropManager = Service.Get(); private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -49,8 +49,6 @@ public sealed class UiBuilder : IDisposable this.stopwatch = new Stopwatch(); this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); this.namespaceName = namespaceName; - this.dragDropManager = new DragDropManager(this); - this.dragDropManager.Enable(); this.interfaceManager.Draw += this.OnDraw; this.interfaceManager.BuildFonts += this.OnBuildFonts; @@ -406,7 +404,6 @@ public sealed class UiBuilder : IDisposable /// void IDisposable.Dispose() { - this.dragDropManager.Dispose(); this.interfaceManager.Draw -= this.OnDraw; this.interfaceManager.BuildFonts -= this.OnBuildFonts; this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; From ca69aae50039e34df2fcf9a856947bc347f0f1a0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 May 2023 21:35:01 +0200 Subject: [PATCH 04/81] Remove some warnings. --- Dalamud/Interface/DragDrop/DragDropInterop.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs index 4a14d5e37..8bf04f160 100644 --- a/Dalamud/Interface/DragDrop/DragDropInterop.cs +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -9,6 +9,7 @@ using System.Text; // ReSharper disable InconsistentNaming namespace Dalamud.Interface.DragDrop; +#pragma warning disable SA1600 // Elements should be documented /// Implements interop enums and function calls to interact with external drag and drop. internal partial class DragDropManager { @@ -27,10 +28,13 @@ internal partial class DragDropManager { [MethodImpl(MethodImplOptions.InternalCall)] void DragEnter([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect); + [MethodImpl(MethodImplOptions.InternalCall)] void DragOver([ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect); + [MethodImpl(MethodImplOptions.InternalCall)] void DragLeave(); + [MethodImpl(MethodImplOptions.InternalCall)] void Drop([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect); } @@ -100,3 +104,4 @@ internal partial class DragDropManager public static extern int DragQueryFile(IntPtr hDrop, uint iFile, StringBuilder lpszFile, int cch); } } +#pragma warning restore SA1600 // Elements should be documented From 4b3253b9e73be4ea08d72cc50fdd89c32d12861b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 May 2023 22:01:12 +0200 Subject: [PATCH 05/81] Fix some problems. --- Dalamud/Interface/DragDrop/DragDropManager.cs | 15 ++++++++------- Dalamud/Interface/DragDrop/DragDropTarget.cs | 16 ++++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 2b1f79ed1..0bb7eac12 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -35,16 +35,17 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService /// Gets a value indicating whether a valid external drag and drop is currently active and hovering over any FFXIV-related viewport. public bool IsDragging { get; private set; } - /// Gets a value indicating whether there are any files or directories currently being dragged. - public bool HasPaths { get; private set; } + /// Gets a value indicating whether there are any files or directories currently being dragged, or stored from the last drop. + public bool HasPaths + => this.Files.Count + this.Directories.Count > 0; - /// Gets the list of file paths currently being dragged from an external application over any FFXIV-related viewport. + /// Gets the list of file paths currently being dragged from an external application over any FFXIV-related viewport, or stored from the last drop. public IReadOnlyList Files { get; private set; } = Array.Empty(); - /// Gets a set of all extensions available in the paths currently being dragged from an external application over any FFXIV-related viewport. + /// Gets a set of all extensions available in the paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. public IReadOnlySet Extensions { get; private set; } = new HashSet(); - /// Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport. + /// Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. public IReadOnlyList Directories { get; private set; } = Array.Empty(); /// Enable external drag and drop. @@ -96,7 +97,7 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService /// public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder) { - if (!this.HasPaths && !this.IsDropping()) + if (!this.IsDragging && !this.IsDropping()) { return; } @@ -120,7 +121,7 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService { files = Array.Empty(); directories = Array.Empty(); - if (!this.IsDragging || !ImGui.BeginDragDropTarget()) + if (!this.HasPaths || !ImGui.BeginDragDropTarget()) { return false; } diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index a592991dd..b6bb9d237 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices.ComTypes; using System.Text; @@ -15,6 +16,7 @@ namespace Dalamud.Interface.DragDrop; internal partial class DragDropManager : DragDropManager.IDropTarget { private int lastUpdateFrame = -1; + private DragDropInterop.ModifierKeys lastKeyState = DragDropInterop.ModifierKeys.MK_NONE; /// Create the drag and drop formats we accept. private static FORMATETC FormatEtc = @@ -37,7 +39,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget public void DragEnter(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect) { this.IsDragging = true; - UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, true); + this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, true); if (pDataObj.QueryGetData(ref FormatEtc) != 0) { @@ -47,9 +49,9 @@ internal partial class DragDropManager : DragDropManager.IDropTarget { pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; (this.Files, this.Directories) = this.GetPaths(pDataObj); - this.HasPaths = this.Files.Count + this.Directories.Count > 0; this.Extensions = this.Files.Select(Path.GetExtension).Where(p => !p.IsNullOrEmpty()).Distinct().ToHashSet(); } + Log.Debug("[DragDrop] Entering external Drag and Drop with {KeyState} at {PtX}, {PtY} and with {N} files.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y, this.Files.Count + this.Directories.Count); } @@ -64,7 +66,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget if (frame != this.lastUpdateFrame) { this.lastUpdateFrame = frame; - UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false); + this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false); pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; Log.Verbose("[DragDrop] External Drag and Drop with {KeyState} at {PtX}, {PtY}.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y); } @@ -87,10 +89,10 @@ internal partial class DragDropManager : DragDropManager.IDropTarget /// Effects that can be used with this drag and drop process. public void Drop(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect) { - MouseDrop((DragDropInterop.ModifierKeys)grfKeyState); + MouseDrop(this.lastKeyState); this.lastDropFrame = ImGui.GetFrameCount(); this.IsDragging = false; - if (this.Files.Count > 0 || this.Directories.Count > 0) + if (this.HasPaths) { pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; } @@ -102,7 +104,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget Log.Debug("[DragDrop] Dropping {N} files with {KeyState} at {PtX}, {PtY}.", this.Files.Count + this.Directories.Count, (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y); } - private static void UpdateIo(DragDropInterop.ModifierKeys keys, bool entering) + private static DragDropInterop.ModifierKeys UpdateIo(DragDropInterop.ModifierKeys keys, bool entering) { var io = ImGui.GetIO(); void UpdateMouse(int mouseIdx) @@ -163,6 +165,8 @@ internal partial class DragDropManager : DragDropManager.IDropTarget io.KeyShift = false; io.AddKeyEvent(ImGuiKey.LeftShift, false); } + + return keys; } private static void MouseDrop(DragDropInterop.ModifierKeys keys) From 07a92ba0258080d2b4c649b3e193176eeacc73a2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Jun 2023 00:01:01 +0200 Subject: [PATCH 06/81] Let DI handle lifetime and make Plugin Service. --- Dalamud/Dalamud.cs | 3 -- Dalamud/Interface/DragDrop/DragDropManager.cs | 30 +++++++++---------- Dalamud/Interface/UiBuilder.cs | 6 ---- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index d6cf6a107..4e491f12a 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -151,9 +151,6 @@ internal sealed class Dalamud : IServiceType // will not receive any windows messages Service.GetNullable()?.Dispose(); - // this must be done before unloading interface manager, since it relies on the window handle members. - Service.GetNullable()?.Dispose(); - // this must be done before unloading plugins, or it can cause a race condition // due to rendering happening on another thread, where a plugin might receive // a render call after it has been disposed, which can crash if it attempts to diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 0bb7eac12..8a4a5ca51 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; -using System.Threading.Tasks; + using Dalamud.Interface.Internal; +using Dalamud.IoC; +using Dalamud.IoC.Internal; using ImGuiNET; using Serilog; @@ -12,22 +14,19 @@ namespace Dalamud.Interface.DragDrop; /// A manager that keeps state of external windows drag and drop events, /// and can be used to create ImGui drag and drop sources and targets for those external events. /// +[PluginInterface] [ServiceManager.EarlyLoadedService] +[InherentDependency] internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType { - private InterfaceManager? interfaceManager; + private readonly InterfaceManager interfaceManager = Service.Get(); private int lastDropFrame = -2; private int lastTooltipFrame = -1; + [ServiceManager.ServiceConstructor] private DragDropManager() - { - Service.GetAsync().ContinueWith(task => - { - this.interfaceManager = task.Result.Manager; - this.Enable(); - }); - } + => this.Enable(); /// Gets a value indicating whether external drag and drop is available at all. public bool ServiceAvailable { get; private set; } @@ -51,16 +50,16 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService /// Enable external drag and drop. public void Enable() { - if (this.ServiceAvailable || this.interfaceManager == null) + if (this.ServiceAvailable) { return; } try { - var ret2 = DragDropInterop.RegisterDragDrop(this.interfaceManager.WindowHandlePtr, this); - Log.Information($"[DragDrop] Registered window {this.interfaceManager.WindowHandlePtr} for external drag and drop operations. ({ret2})"); - Marshal.ThrowExceptionForHR(ret2); + var ret = DragDropInterop.RegisterDragDrop(this.interfaceManager.WindowHandlePtr, this); + Log.Information($"[DragDrop] Registered window {this.interfaceManager.WindowHandlePtr} for external drag and drop operations. ({ret})"); + Marshal.ThrowExceptionForHR(ret); this.ServiceAvailable = true; } catch (Exception ex) @@ -79,8 +78,9 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService try { - DragDropInterop.RevokeDragDrop(this.interfaceManager!.WindowHandlePtr); - Log.Information($"[DragDrop] Disabled external drag and drop operations for window {this.interfaceManager.WindowHandlePtr}."); + var ret = DragDropInterop.RevokeDragDrop(this.interfaceManager!.WindowHandlePtr); + Log.Information($"[DragDrop] Disabled external drag and drop operations for window {this.interfaceManager.WindowHandlePtr}. ({ret})"); + Marshal.ThrowExceptionForHR(ret); } catch (Exception ex) { diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 5a757e0e6..53f223ef2 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -31,7 +31,6 @@ public sealed class UiBuilder : IDisposable private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); private readonly GameFontManager gameFontManager = Service.Get(); - private readonly DragDropManager dragDropManager = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); @@ -102,11 +101,6 @@ public sealed class UiBuilder : IDisposable /// public event Action HideUi; - /// - /// Gets the manager for external, WinAPI-based drag and drop functionality. - /// - public IDragDropManager DragDropManager => this.dragDropManager; - /// /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. /// From b99d62e45005ba07b870a08aa1de9a2e383be1a7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 13 Jun 2023 20:55:22 +0200 Subject: [PATCH 07/81] Some finetuning. --- Dalamud/Interface/DragDrop/DragDropInterop.cs | 16 ++++++++-------- Dalamud/Interface/DragDrop/DragDropTarget.cs | 7 +++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs index 8bf04f160..28a2644a5 100644 --- a/Dalamud/Interface/DragDrop/DragDropInterop.cs +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -13,14 +13,6 @@ namespace Dalamud.Interface.DragDrop; /// Implements interop enums and function calls to interact with external drag and drop. internal partial class DragDropManager { - internal struct POINTL - { - [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] - public int x; - [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] - public int y; - } - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("00000122-0000-0000-C000-000000000046")] [ComImport] @@ -39,6 +31,14 @@ internal partial class DragDropManager void Drop([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect); } + internal struct POINTL + { + [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] + public int x; + [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] + public int y; + } + private static class DragDropInterop { [Flags] diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index b6bb9d237..05e5599f9 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices.ComTypes; using System.Text; @@ -19,7 +18,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget private DragDropInterop.ModifierKeys lastKeyState = DragDropInterop.ModifierKeys.MK_NONE; /// Create the drag and drop formats we accept. - private static FORMATETC FormatEtc = + private FORMATETC formatEtc = new() { cfFormat = (short)DragDropInterop.ClipboardFormat.CF_HDROP, @@ -41,7 +40,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget this.IsDragging = true; this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, true); - if (pDataObj.QueryGetData(ref FormatEtc) != 0) + if (pDataObj.QueryGetData(ref this.formatEtc) != 0) { pdwEffect = 0; } @@ -203,7 +202,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget try { - data.GetData(ref FormatEtc, out var stgMedium); + data.GetData(ref this.formatEtc, out var stgMedium); var numFiles = DragDropInterop.DragQueryFile(stgMedium.unionmember, uint.MaxValue, new StringBuilder(), 0); var files = new string[numFiles]; var sb = new StringBuilder(1024); From 20186afa490344267be267cf0c1aa9e63d737d8b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 13 Jun 2023 20:55:37 +0200 Subject: [PATCH 08/81] Resolve as service. --- Dalamud/Interface/DragDrop/DragDropManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 8a4a5ca51..9134d5315 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -17,6 +17,7 @@ namespace Dalamud.Interface.DragDrop; [PluginInterface] [ServiceManager.EarlyLoadedService] [InherentDependency] +[ResolveVia] internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType { private readonly InterfaceManager interfaceManager = Service.Get(); From 90722fcfbe2c613b9313487ed0c35fa88b47ae6f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 13 Jun 2023 21:22:12 +0200 Subject: [PATCH 09/81] Change to actively inject WithScene. --- Dalamud/Interface/DragDrop/DragDropManager.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 9134d5315..4363cfabc 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -16,11 +16,11 @@ namespace Dalamud.Interface.DragDrop; /// [PluginInterface] [ServiceManager.EarlyLoadedService] -[InherentDependency] [ResolveVia] internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType { - private readonly InterfaceManager interfaceManager = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly InterfaceManager.InterfaceManagerWithScene interfaceManager = Service.Get(); private int lastDropFrame = -2; private int lastTooltipFrame = -1; @@ -58,14 +58,14 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService try { - var ret = DragDropInterop.RegisterDragDrop(this.interfaceManager.WindowHandlePtr, this); - Log.Information($"[DragDrop] Registered window {this.interfaceManager.WindowHandlePtr} for external drag and drop operations. ({ret})"); + var ret = DragDropInterop.RegisterDragDrop(this.interfaceManager.Manager.WindowHandlePtr, this); + Log.Information($"[DragDrop] Registered window 0x{this.interfaceManager.Manager.WindowHandlePtr:X} for external drag and drop operations. ({ret})"); Marshal.ThrowExceptionForHR(ret); this.ServiceAvailable = true; } catch (Exception ex) { - Log.Error($"Could not create windows drag and drop utility:\n{ex}"); + Log.Error($"Could not create windows drag and drop utility for window 0x{this.interfaceManager.Manager.WindowHandlePtr:X}:\n{ex}"); } } @@ -79,13 +79,13 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService try { - var ret = DragDropInterop.RevokeDragDrop(this.interfaceManager!.WindowHandlePtr); - Log.Information($"[DragDrop] Disabled external drag and drop operations for window {this.interfaceManager.WindowHandlePtr}. ({ret})"); + var ret = DragDropInterop.RevokeDragDrop(this.interfaceManager.Manager.WindowHandlePtr); + Log.Information($"[DragDrop] Disabled external drag and drop operations for window 0x{this.interfaceManager.Manager.WindowHandlePtr:X}. ({ret})"); Marshal.ThrowExceptionForHR(ret); } catch (Exception ex) { - Log.Error($"Could not disable windows drag and drop utility:\n{ex}"); + Log.Error($"Could not disable windows drag and drop utility for window 0x{this.interfaceManager.Manager.WindowHandlePtr:X}:\n{ex}"); } this.ServiceAvailable = false; From d7ce12a2ea6991cbae7a234cc2d5b851b7ab0aa9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 13 Jun 2023 21:23:14 +0200 Subject: [PATCH 10/81] Whitespace... --- Dalamud/Interface/DragDrop/DragDropManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 4363cfabc..34f1296e1 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -21,10 +21,10 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService { [ServiceManager.ServiceDependency] private readonly InterfaceManager.InterfaceManagerWithScene interfaceManager = Service.Get(); + private int lastDropFrame = -2; private int lastTooltipFrame = -1; - [ServiceManager.ServiceConstructor] private DragDropManager() => this.Enable(); From 1d5c3cee1114bc6d9f762d9c61089a6c8527c34b Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:10:53 -0700 Subject: [PATCH 11/81] Add IJobGauges (#1265) --- .../Game/ClientState/JobGauge/JobGauges.cs | 16 +++++++------- Dalamud/Plugin/Services/IJobGauges.cs | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 Dalamud/Plugin/Services/IJobGauges.cs diff --git a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs index bf5c4b525..683f5c61f 100644 --- a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs +++ b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs @@ -5,6 +5,7 @@ using System.Reflection; using Dalamud.Game.ClientState.JobGauge.Types; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.ClientState.JobGauge; @@ -15,7 +16,10 @@ namespace Dalamud.Game.ClientState.JobGauge; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public class JobGauges : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public class JobGauges : IServiceType, IJobGauges { private Dictionary cache = new(); @@ -27,16 +31,10 @@ public class JobGauges : IServiceType Log.Verbose($"JobGaugeData address 0x{this.Address.ToInt64():X}"); } - /// - /// Gets the address of the JobGauge data. - /// + /// public IntPtr Address { get; } - /// - /// Get the JobGauge for a given job. - /// - /// A JobGauge struct from ClientState.Structs.JobGauge. - /// A JobGauge. + /// public T Get() where T : JobGaugeBase { // This is cached to mitigate the effects of using activator for instantiation. diff --git a/Dalamud/Plugin/Services/IJobGauges.cs b/Dalamud/Plugin/Services/IJobGauges.cs new file mode 100644 index 000000000..4489a7be7 --- /dev/null +++ b/Dalamud/Plugin/Services/IJobGauges.cs @@ -0,0 +1,21 @@ +using Dalamud.Game.ClientState.JobGauge.Types; + +namespace Dalamud.Plugin.Services; + +/// +/// This class converts in-memory Job gauge data to structs. +/// +public interface IJobGauges +{ + /// + /// Gets the address of the JobGauge data. + /// + public nint Address { get; } + + /// + /// Get the JobGauge for a given job. + /// + /// A JobGauge struct from ClientState.Structs.JobGauge. + /// A JobGauge. + public T Get() where T : JobGaugeBase; +} From a7202096bf33d2a0f3e80886560f665e71ba72e9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:11:05 -0700 Subject: [PATCH 12/81] Add IFateTable (#1263) --- Dalamud/Game/ClientState/Fates/FateTable.cs | 34 ++++++----------- Dalamud/Plugin/Services/IFateTable.cs | 42 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 Dalamud/Plugin/Services/IFateTable.cs diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index dfd4bcaee..8416f0ffb 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.ClientState.Fates; @@ -14,7 +15,10 @@ namespace Dalamud.Game.ClientState.Fates; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed partial class FateTable : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning enable SA1015 +public sealed partial class FateTable : IServiceType, IFateTable { private readonly ClientStateAddressResolver address; @@ -26,14 +30,10 @@ public sealed partial class FateTable : IServiceType Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}"); } - /// - /// Gets the address of the Fate table. - /// + /// public IntPtr Address => this.address.FateTablePtr; - /// - /// Gets the amount of currently active Fates. - /// + /// public unsafe int Length { get @@ -69,11 +69,7 @@ public sealed partial class FateTable : IServiceType private unsafe FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager*)this.FateTableAddress; - /// - /// Get an actor at the specified spawn index. - /// - /// Spawn index. - /// A at the specified spawn index. + /// public Fate? this[int index] { get @@ -83,11 +79,7 @@ public sealed partial class FateTable : IServiceType } } - /// - /// Gets the address of the Fate at the specified index of the fate table. - /// - /// The index of the Fate. - /// The memory address of the Fate. + /// public unsafe IntPtr GetFateAddress(int index) { if (index >= this.Length) @@ -100,11 +92,7 @@ public sealed partial class FateTable : IServiceType return (IntPtr)this.Struct->Fates.Get((ulong)index).Value; } - /// - /// Create a reference to a FFXIV actor. - /// - /// The offset of the actor in memory. - /// object containing requested data. + /// public Fate? CreateFateReference(IntPtr offset) { var clientState = Service.Get(); @@ -122,7 +110,7 @@ public sealed partial class FateTable : IServiceType /// /// This collection represents the currently available Fate events. /// -public sealed partial class FateTable : IReadOnlyCollection +public sealed partial class FateTable { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Plugin/Services/IFateTable.cs b/Dalamud/Plugin/Services/IFateTable.cs new file mode 100644 index 000000000..ba243ec04 --- /dev/null +++ b/Dalamud/Plugin/Services/IFateTable.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +using Dalamud.Game.ClientState.Fates; + +namespace Dalamud.Plugin.Services; + +/// +/// This collection represents the currently available Fate events. +/// +public interface IFateTable : IReadOnlyCollection +{ + /// + /// Gets the address of the Fate table. + /// + public nint Address { get; } + + /// + /// Gets the amount of currently active Fates. + /// + public int Length { get; } + + /// + /// Get an actor at the specified spawn index. + /// + /// Spawn index. + /// A at the specified spawn index. + public Fate? this[int index] { get; } + + /// + /// Gets the address of the Fate at the specified index of the fate table. + /// wo + /// The index of the Fate. + /// The memory address of the Fate. + public nint GetFateAddress(int index); + + /// + /// Create a reference to a FFXIV actor. + /// + /// The offset of the actor in memory. + /// object containing requested data. + public Fate? CreateFateReference(nint offset); +} From ac74bd5fe02f01be00ed77bddf0bbfa652ae36a7 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:11:17 -0700 Subject: [PATCH 13/81] Add IBuddyList (#1261) --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 50 ++++++----------- Dalamud/Plugin/Services/IBuddyList.cs | 61 +++++++++++++++++++++ 2 files changed, 77 insertions(+), 34 deletions(-) create mode 100644 Dalamud/Plugin/Services/IBuddyList.cs diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 4c8de5586..dc2cb9fae 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.ClientState.Buddy; @@ -16,7 +17,10 @@ namespace Dalamud.Game.ClientState.Buddy; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed partial class BuddyList : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed partial class BuddyList : IServiceType, IBuddyList { private const uint InvalidObjectID = 0xE0000000; @@ -33,9 +37,7 @@ public sealed partial class BuddyList : IServiceType Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}"); } - /// - /// Gets the amount of battle buddies the local player has. - /// + /// public int Length { get @@ -56,16 +58,16 @@ public sealed partial class BuddyList : IServiceType /// /// Gets a value indicating whether the local player's companion is present. /// + [Obsolete("Use CompanionBuddy != null", false)] public bool CompanionBuddyPresent => this.CompanionBuddy != null; /// /// Gets a value indicating whether the local player's pet is present. /// + [Obsolete("Use PetBuddy != null", false)] public bool PetBuddyPresent => this.PetBuddy != null; - /// - /// Gets the active companion buddy. - /// + /// public BuddyMember? CompanionBuddy { get @@ -75,9 +77,7 @@ public sealed partial class BuddyList : IServiceType } } - /// - /// Gets the active pet buddy. - /// + /// public BuddyMember? PetBuddy { get @@ -96,11 +96,7 @@ public sealed partial class BuddyList : IServiceType private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy*)this.BuddyListAddress; - /// - /// Gets a battle buddy at the specified spawn index. - /// - /// Spawn index. - /// A at the specified spawn index. + /// public BuddyMember? this[int index] { get @@ -110,29 +106,19 @@ public sealed partial class BuddyList : IServiceType } } - /// - /// Gets the address of the companion buddy. - /// - /// The memory address of the companion buddy. + /// public unsafe IntPtr GetCompanionBuddyMemberAddress() { return (IntPtr)(&this.BuddyListStruct->Companion); } - /// - /// Gets the address of the pet buddy. - /// - /// The memory address of the pet buddy. + /// public unsafe IntPtr GetPetBuddyMemberAddress() { return (IntPtr)(&this.BuddyListStruct->Pet); } - /// - /// Gets the address of the battle buddy at the specified index of the buddy list. - /// - /// The index of the battle buddy. - /// The memory address of the battle buddy. + /// public unsafe IntPtr GetBattleBuddyMemberAddress(int index) { if (index < 0 || index >= 3) @@ -141,11 +127,7 @@ public sealed partial class BuddyList : IServiceType return (IntPtr)(this.BuddyListStruct->BattleBuddies + (index * BuddyMemberSize)); } - /// - /// Create a reference to a buddy. - /// - /// The address of the buddy in memory. - /// object containing the requested data. + /// public BuddyMember? CreateBuddyMemberReference(IntPtr address) { if (this.clientState.LocalContentId == 0) @@ -165,7 +147,7 @@ public sealed partial class BuddyList : IServiceType /// /// This collection represents the buddies present in your squadron or trust party. /// -public sealed partial class BuddyList : IReadOnlyCollection +public sealed partial class BuddyList { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Plugin/Services/IBuddyList.cs b/Dalamud/Plugin/Services/IBuddyList.cs new file mode 100644 index 000000000..f273d71c9 --- /dev/null +++ b/Dalamud/Plugin/Services/IBuddyList.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Game.ClientState.Buddy; + +namespace Dalamud.Plugin.Services; + +/// +/// This collection represents the buddies present in your squadron or trust party. +/// It does not include the local player. +/// +public interface IBuddyList : IReadOnlyCollection +{ + /// + /// Gets the amount of battle buddies the local player has. + /// + public int Length { get; } + + /// + /// Gets the active companion buddy. + /// + public BuddyMember? CompanionBuddy { get; } + + /// + /// Gets the active pet buddy. + /// + public BuddyMember? PetBuddy { get; } + + /// + /// Gets a battle buddy at the specified spawn index. + /// + /// Spawn index. + /// A at the specified spawn index. + public BuddyMember? this[int index] { get; } + + /// + /// Gets the address of the companion buddy. + /// + /// The memory address of the companion buddy. + public nint GetCompanionBuddyMemberAddress(); + + /// + /// Gets the address of the pet buddy. + /// + /// The memory address of the pet buddy. + public nint GetPetBuddyMemberAddress(); + + /// + /// Gets the address of the battle buddy at the specified index of the buddy list. + /// + /// The index of the battle buddy. + /// The memory address of the battle buddy. + public nint GetBattleBuddyMemberAddress(int index); + + /// + /// Create a reference to a buddy. + /// + /// The address of the buddy in memory. + /// object containing the requested data. + public BuddyMember? CreateBuddyMemberReference(nint address); +} From 2f138bb1df381903c59df463d64c3c3308e8faf2 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:11:55 -0700 Subject: [PATCH 14/81] Add IGamepadState (#1264) --- .../Game/ClientState/GamePad/GamepadState.cs | 64 ++++++++--------- Dalamud/Plugin/Services/IGamepadState.cs | 68 +++++++++++++++++++ 2 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 Dalamud/Plugin/Services/IGamepadState.cs diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index c72e9c1de..bc5744047 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -1,8 +1,10 @@ using System; +using System.Numerics; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using ImGuiNET; using Serilog; @@ -16,9 +18,12 @@ namespace Dalamud.Game.ClientState.GamePad; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public unsafe class GamepadState : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState { - private readonly Hook gamepadPoll; + private readonly Hook? gamepadPoll; private bool isDisposed; @@ -42,44 +47,60 @@ public unsafe class GamepadState : IDisposable, IServiceType /// public IntPtr GamepadInputAddress { get; private set; } + /// + public Vector2 LeftStick => + new(this.leftStickX, this.leftStickY); + + /// + public Vector2 RightStick => + new(this.rightStickX, this.rightStickY); + /// /// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt). /// + [Obsolete("Use IGamepadState.LeftStick.X", false)] public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0; /// /// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt). /// + [Obsolete("Use IGamepadState.LeftStick.X", false)] public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0; /// /// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt). /// + [Obsolete("Use IGamepadState.LeftStick.Y", false)] public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0; /// /// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt). /// + [Obsolete("Use IGamepadState.LeftStick.Y", false)] public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0; /// /// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt). /// + [Obsolete("Use IGamepadState.RightStick.X", false)] public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0; /// /// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt). /// + [Obsolete("Use IGamepadState.RightStick.X", false)] public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0; /// /// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt). /// + [Obsolete("Use IGamepadState.RightStick.Y", false)] public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0; /// /// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt). /// + [Obsolete("Use IGamepadState.RightStick.Y", false)] public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0; /// @@ -120,43 +141,16 @@ public unsafe class GamepadState : IDisposable, IServiceType /// internal bool NavEnableGamepad { get; set; } - /// - /// Gets whether has been pressed. - /// - /// Only true on first frame of the press. - /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable. - /// - /// The button to check for. - /// 1 if pressed, 0 otherwise. + /// public float Pressed(GamepadButtons button) => (this.ButtonsPressed & (ushort)button) > 0 ? 1 : 0; - /// - /// Gets whether is being pressed. - /// - /// True in intervals if button is held down. - /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable. - /// - /// The button to check for. - /// 1 if still pressed during interval, 0 otherwise or in between intervals. + /// public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0; - /// - /// Gets whether has been released. - /// - /// Only true the frame after release. - /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable. - /// - /// The button to check for. - /// 1 if released, 0 otherwise. + /// public float Released(GamepadButtons button) => (this.ButtonsReleased & (ushort)button) > 0 ? 1 : 0; - /// - /// Gets the raw state of . - /// - /// Is set the entire time a button is pressed down. - /// - /// The button to check for. - /// 1 the whole time button is pressed, 0 otherwise. + /// public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0; /// @@ -171,12 +165,12 @@ public unsafe class GamepadState : IDisposable, IServiceType [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { - this.gamepadPoll.Enable(); + this.gamepadPoll?.Enable(); } private int GamepadPollDetour(IntPtr gamepadInput) { - var original = this.gamepadPoll.Original(gamepadInput); + var original = this.gamepadPoll!.Original(gamepadInput); try { this.GamepadInputAddress = gamepadInput; diff --git a/Dalamud/Plugin/Services/IGamepadState.cs b/Dalamud/Plugin/Services/IGamepadState.cs new file mode 100644 index 000000000..c349923f3 --- /dev/null +++ b/Dalamud/Plugin/Services/IGamepadState.cs @@ -0,0 +1,68 @@ +using System.Numerics; + +using Dalamud.Game.ClientState.GamePad; +using ImGuiNET; + +namespace Dalamud.Plugin.Services; + +/// +/// Exposes the game gamepad state to dalamud. +/// +/// Will block game's gamepad input if is set. +/// +public interface IGamepadState +{ + /// + /// Gets the pointer to the current instance of the GamepadInput struct. + /// + public nint GamepadInputAddress { get; } + + /// + /// Gets the left analogue sticks tilt vector. + /// + public Vector2 LeftStick { get; } + + /// + /// Gets the right analogue sticks tilt vector. + /// + public Vector2 RightStick { get; } + + /// + /// Gets whether has been pressed. + /// + /// Only true on first frame of the press. + /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable. + /// + /// The button to check for. + /// 1 if pressed, 0 otherwise. + public float Pressed(GamepadButtons button); + + /// + /// Gets whether is being pressed. + /// + /// True in intervals if button is held down. + /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable. + /// + /// The button to check for. + /// 1 if still pressed during interval, 0 otherwise or in between intervals. + public float Repeat(GamepadButtons button); + + /// + /// Gets whether has been released. + /// + /// Only true the frame after release. + /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable. + /// + /// The button to check for. + /// 1 if released, 0 otherwise. + public float Released(GamepadButtons button); + + /// + /// Gets the raw state of . + /// + /// Is set the entire time a button is pressed down. + /// + /// The button to check for. + /// 1 the whole time button is pressed, 0 otherwise. + public float Raw(GamepadButtons button); +} From aacfe4d6799b3c98ede0b6886c93175957c1dfff Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 20:59:00 -0700 Subject: [PATCH 15/81] Add IObjectTable (#1270) --- .../Game/ClientState/Objects/ObjectTable.cs | 40 +++++---------- Dalamud/Plugin/Services/IObjectTable.cs | 49 +++++++++++++++++++ 2 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 Dalamud/Plugin/Services/IObjectTable.cs diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 70c17fd83..16cf7c277 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -7,6 +7,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.ClientState.Objects; @@ -17,7 +18,10 @@ namespace Dalamud.Game.ClientState.Objects; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed partial class ObjectTable : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed partial class ObjectTable : IServiceType, IObjectTable { private const int ObjectTableLength = 596; @@ -31,21 +35,13 @@ public sealed partial class ObjectTable : IServiceType Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}"); } - /// - /// Gets the address of the object table. - /// + /// public IntPtr Address => this.address.ObjectTable; - /// - /// Gets the length of the object table. - /// + /// public int Length => ObjectTableLength; - /// - /// Get an object at the specified spawn index. - /// - /// Spawn index. - /// An at the specified spawn index. + /// public GameObject? this[int index] { get @@ -55,11 +51,7 @@ public sealed partial class ObjectTable : IServiceType } } - /// - /// Search for a game object by their Object ID. - /// - /// Object ID to find. - /// A game object or null. + /// public GameObject? SearchById(ulong objectId) { if (objectId is GameObject.InvalidGameObjectId or 0) @@ -77,11 +69,7 @@ public sealed partial class ObjectTable : IServiceType return null; } - /// - /// Gets the address of the game object at the specified index of the object table. - /// - /// The index of the object. - /// The memory address of the object. + /// public unsafe IntPtr GetObjectAddress(int index) { if (index < 0 || index >= ObjectTableLength) @@ -90,11 +78,7 @@ public sealed partial class ObjectTable : IServiceType return *(IntPtr*)(this.address.ObjectTable + (8 * index)); } - /// - /// Create a reference to an FFXIV game object. - /// - /// The address of the object in memory. - /// object or inheritor containing the requested data. + /// public unsafe GameObject? CreateObjectReference(IntPtr address) { var clientState = Service.GetNullable(); @@ -125,7 +109,7 @@ public sealed partial class ObjectTable : IServiceType /// /// This collection represents the currently spawned FFXIV game objects. /// -public sealed partial class ObjectTable : IReadOnlyCollection +public sealed partial class ObjectTable { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Plugin/Services/IObjectTable.cs b/Dalamud/Plugin/Services/IObjectTable.cs new file mode 100644 index 000000000..d029045fa --- /dev/null +++ b/Dalamud/Plugin/Services/IObjectTable.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; + +using Dalamud.Game.ClientState.Objects.Types; + +namespace Dalamud.Plugin.Services; + +/// +/// This collection represents the currently spawned FFXIV game objects. +/// +public interface IObjectTable : IReadOnlyCollection +{ + /// + /// Gets the address of the object table. + /// + public nint Address { get; } + + /// + /// Gets the length of the object table. + /// + public int Length { get; } + + /// + /// Get an object at the specified spawn index. + /// + /// Spawn index. + /// An at the specified spawn index. + public GameObject? this[int index] { get; } + + /// + /// Search for a game object by their Object ID. + /// + /// Object ID to find. + /// A game object or null. + public GameObject? SearchById(ulong objectId); + + /// + /// Gets the address of the game object at the specified index of the object table. + /// + /// The index of the object. + /// The memory address of the object. + public nint GetObjectAddress(int index); + + /// + /// Create a reference to an FFXIV game object. + /// + /// The address of the object in memory. + /// object or inheritor containing the requested data. + public GameObject? CreateObjectReference(nint address); +} From 852d68f32682aca47717af65c0c03d06e506c0da Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 20:59:37 -0700 Subject: [PATCH 16/81] Add IPartyList (#1271) --- Dalamud/Game/ClientState/Party/PartyList.cs | 66 +++++------------ Dalamud/Plugin/Services/IPartyList.cs | 81 +++++++++++++++++++++ 2 files changed, 99 insertions(+), 48 deletions(-) create mode 100644 Dalamud/Plugin/Services/IPartyList.cs diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 80fe7d41f..529b57b6f 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.ClientState.Party; @@ -15,7 +16,10 @@ namespace Dalamud.Game.ClientState.Party; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed unsafe partial class PartyList : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed unsafe partial class PartyList : IServiceType, IPartyList { private const int GroupLength = 8; private const int AllianceLength = 20; @@ -33,50 +37,32 @@ public sealed unsafe partial class PartyList : IServiceType Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}"); } - /// - /// Gets the amount of party members the local player has. - /// + /// public int Length => this.GroupManagerStruct->MemberCount; - /// - /// Gets the index of the party leader. - /// + /// public uint PartyLeaderIndex => this.GroupManagerStruct->PartyLeaderIndex; - /// - /// Gets a value indicating whether this group is an alliance. - /// + /// public bool IsAlliance => this.GroupManagerStruct->AllianceFlags > 0; - /// - /// Gets the address of the Group Manager. - /// + /// public IntPtr GroupManagerAddress => this.address.GroupManager; - /// - /// Gets the address of the party list within the group manager. - /// + /// public IntPtr GroupListAddress => (IntPtr)GroupManagerStruct->PartyMembers; - /// - /// Gets the address of the alliance member list within the group manager. - /// + /// public IntPtr AllianceListAddress => (IntPtr)this.GroupManagerStruct->AllianceMembers; - /// - /// Gets the ID of the party. - /// + /// public long PartyId => this.GroupManagerStruct->PartyId; private static int PartyMemberSize { get; } = Marshal.SizeOf(); private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress; - /// - /// Get a party member at the specified spawn index. - /// - /// Spawn index. - /// A at the specified spawn index. + /// public PartyMember? this[int index] { get @@ -98,11 +84,7 @@ public sealed unsafe partial class PartyList : IServiceType } } - /// - /// Gets the address of the party member at the specified index of the party list. - /// - /// The index of the party member. - /// The memory address of the party member. + /// public IntPtr GetPartyMemberAddress(int index) { if (index < 0 || index >= GroupLength) @@ -111,11 +93,7 @@ public sealed unsafe partial class PartyList : IServiceType return this.GroupListAddress + (index * PartyMemberSize); } - /// - /// Create a reference to an FFXIV party member. - /// - /// The address of the party member in memory. - /// The party member object containing the requested data. + /// public PartyMember? CreatePartyMemberReference(IntPtr address) { if (this.clientState.LocalContentId == 0) @@ -127,11 +105,7 @@ public sealed unsafe partial class PartyList : IServiceType return new PartyMember(address); } - /// - /// Gets the address of the alliance member at the specified index of the alliance list. - /// - /// The index of the alliance member. - /// The memory address of the alliance member. + /// public IntPtr GetAllianceMemberAddress(int index) { if (index < 0 || index >= AllianceLength) @@ -140,11 +114,7 @@ public sealed unsafe partial class PartyList : IServiceType return this.AllianceListAddress + (index * PartyMemberSize); } - /// - /// Create a reference to an FFXIV alliance member. - /// - /// The address of the alliance member in memory. - /// The party member object containing the requested data. + /// public PartyMember? CreateAllianceMemberReference(IntPtr address) { if (this.clientState.LocalContentId == 0) @@ -160,7 +130,7 @@ public sealed unsafe partial class PartyList : IServiceType /// /// This collection represents the party members present in your party or alliance. /// -public sealed partial class PartyList : IReadOnlyCollection +public sealed partial class PartyList { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Plugin/Services/IPartyList.cs b/Dalamud/Plugin/Services/IPartyList.cs new file mode 100644 index 000000000..fbf663a00 --- /dev/null +++ b/Dalamud/Plugin/Services/IPartyList.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; + +using Dalamud.Game.ClientState.Party; + +namespace Dalamud.Plugin.Services; + +/// +/// This collection represents the actors present in your party or alliance. +/// +public interface IPartyList : IReadOnlyCollection +{ + /// + /// Gets the amount of party members the local player has. + /// + public int Length { get; } + + /// + /// Gets the index of the party leader. + /// + public uint PartyLeaderIndex { get; } + + /// + /// Gets a value indicating whether this group is an alliance. + /// + public bool IsAlliance { get; } + + /// + /// Gets the address of the Group Manager. + /// + public nint GroupManagerAddress { get; } + + /// + /// Gets the address of the party list within the group manager. + /// + public nint GroupListAddress { get; } + + /// + /// Gets the address of the alliance member list within the group manager. + /// + public nint AllianceListAddress { get; } + + /// + /// Gets the ID of the party. + /// + public long PartyId { get; } + + /// + /// Get a party member at the specified spawn index. + /// + /// Spawn index. + /// A at the specified spawn index. + public PartyMember? this[int index] { get; } + + /// + /// Gets the address of the party member at the specified index of the party list. + /// + /// The index of the party member. + /// The memory address of the party member. + public nint GetPartyMemberAddress(int index); + + /// + /// Create a reference to an FFXIV party member. + /// + /// The address of the party member in memory. + /// The party member object containing the requested data. + public PartyMember? CreatePartyMemberReference(nint address); + + /// + /// Gets the address of the alliance member at the specified index of the alliance list. + /// + /// The index of the alliance member. + /// The memory address of the alliance member. + public nint GetAllianceMemberAddress(int index); + + /// + /// Create a reference to an FFXIV alliance member. + /// + /// The address of the alliance member in memory. + /// The party member object containing the requested data. + public PartyMember? CreateAllianceMemberReference(nint address); +} From 6792fb4de58d9ce18b0d51bc47981f76ef53973b Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 21:02:45 -0700 Subject: [PATCH 17/81] Add ICommandManager (#1273) --- Dalamud/Game/Command/CommandManager.cs | 38 ++++++------------ Dalamud/Plugin/Services/ICommandManager.cs | 46 ++++++++++++++++++++++ 2 files changed, 57 insertions(+), 27 deletions(-) create mode 100644 Dalamud/Plugin/Services/ICommandManager.cs diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 7bb429063..11cbbffbd 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -8,6 +8,7 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.Command; @@ -18,7 +19,10 @@ namespace Dalamud.Game.Command; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class CommandManager : IServiceType, IDisposable +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed class CommandManager : IServiceType, IDisposable, ICommandManager { private readonly Dictionary commandMap = new(); private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled); @@ -46,16 +50,10 @@ public sealed class CommandManager : IServiceType, IDisposable this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled; } - /// - /// Gets a read-only list of all registered commands. - /// + /// public ReadOnlyDictionary Commands => new(this.commandMap); - /// - /// Process a command in full. - /// - /// The full command string. - /// True if the command was found and dispatched. + /// public bool ProcessCommand(string content) { string command; @@ -91,19 +89,14 @@ public sealed class CommandManager : IServiceType, IDisposable argument = content[argStart..]; } - if (!this.commandMap.TryGetValue(command, out var handler)) // Commad was not found. + if (!this.commandMap.TryGetValue(command, out var handler)) // Command was not found. return false; this.DispatchCommand(command, argument, handler); return true; } - /// - /// Dispatch the handling of a command. - /// - /// The command to dispatch. - /// The provided arguments. - /// A object describing this command. + /// public void DispatchCommand(string command, string argument, CommandInfo info) { try @@ -116,12 +109,7 @@ public sealed class CommandManager : IServiceType, IDisposable } } - /// - /// Add a command handler, which you can use to add your own custom commands to the in-game chat. - /// - /// The command to register. - /// A object describing the command. - /// If adding was successful. + /// public bool AddHandler(string command, CommandInfo info) { if (info == null) @@ -139,11 +127,7 @@ public sealed class CommandManager : IServiceType, IDisposable } } - /// - /// Remove a command from the command handlers. - /// - /// The command to remove. - /// If the removal was successful. + /// public bool RemoveHandler(string command) { return this.commandMap.Remove(command); diff --git a/Dalamud/Plugin/Services/ICommandManager.cs b/Dalamud/Plugin/Services/ICommandManager.cs new file mode 100644 index 000000000..ead7723eb --- /dev/null +++ b/Dalamud/Plugin/Services/ICommandManager.cs @@ -0,0 +1,46 @@ +using System.Collections.ObjectModel; + +using Dalamud.Game.Command; + +namespace Dalamud.Plugin.Services; + +/// +/// This class manages registered in-game slash commands. +/// +public interface ICommandManager +{ + /// + /// Gets a read-only list of all registered commands. + /// + public ReadOnlyDictionary Commands { get; } + + /// + /// Process a command in full. + /// + /// The full command string. + /// True if the command was found and dispatched. + public bool ProcessCommand(string content); + + /// + /// Dispatch the handling of a command. + /// + /// The command to dispatch. + /// The provided arguments. + /// A object describing this command. + public void DispatchCommand(string command, string argument, CommandInfo info); + + /// + /// Add a command handler, which you can use to add your own custom commands to the in-game chat. + /// + /// The command to register. + /// A object describing the command. + /// If adding was successful. + public bool AddHandler(string command, CommandInfo info); + + /// + /// Remove a command from the command handlers. + /// + /// The command to remove. + /// If the removal was successful. + public bool RemoveHandler(string command); +} From 7ab20e9125819c28a31161075bcb93a8575188a5 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 21:03:25 -0700 Subject: [PATCH 18/81] Add IGameConfig (#1274) --- Dalamud/Game/Config/GameConfig.cs | 228 ++++------------------- Dalamud/Plugin/Services/IGameConfig.cs | 242 +++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 188 deletions(-) create mode 100644 Dalamud/Plugin/Services/IGameConfig.cs diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 81112cd79..5587787c9 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -1,7 +1,6 @@ -using System.Diagnostics; - -using Dalamud.IoC; +using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.Config; @@ -12,14 +11,17 @@ namespace Dalamud.Game.Config; [InterfaceVersion("1.0")] [PluginInterface] [ServiceManager.EarlyLoadedService] -public sealed class GameConfig : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed class GameConfig : IServiceType, IGameConfig { [ServiceManager.ServiceConstructor] private unsafe GameConfig(Framework framework) { framework.RunOnTick(() => { - Log.Verbose("[GameConfig] Initalizing"); + Log.Verbose("[GameConfig] Initializing"); var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); var commonConfig = &csFramework->SystemConfig.CommonSystemConfig; this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase); @@ -28,234 +30,84 @@ public sealed class GameConfig : IServiceType }); } - /// - /// Gets the collection of config options that persist between characters. - /// + /// public GameConfigSection System { get; private set; } - /// - /// Gets the collection of config options that are character specific. - /// + /// public GameConfigSection UiConfig { get; private set; } - /// - /// Gets the collection of config options that are control mode specific. (Mouse and Keyboard / Gamepad). - /// + /// public GameConfigSection UiControl { get; private set; } - /// - /// Attempts to get a boolean config value from the System section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value); - /// - /// Attempts to get a uint config value from the System section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(SystemConfigOption option, out uint value) => this.System.TryGet(option.GetName(), out value); - /// - /// Attempts to get a float config value from the System section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(SystemConfigOption option, out float value) => this.System.TryGet(option.GetName(), out value); - /// - /// Attempts to get a string config value from the System section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(SystemConfigOption option, out string value) => this.System.TryGet(option.GetName(), out value); - /// - /// Attempts to get a boolean config value from the UiConfig section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(UiConfigOption option, out bool value) => this.UiConfig.TryGet(option.GetName(), out value); - /// - /// Attempts to get a uint config value from the UiConfig section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(UiConfigOption option, out uint value) => this.UiConfig.TryGet(option.GetName(), out value); - /// - /// Attempts to get a float config value from the UiConfig section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(UiConfigOption option, out float value) => this.UiConfig.TryGet(option.GetName(), out value); - /// - /// Attempts to get a string config value from the UiConfig section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(UiConfigOption option, out string value) => this.UiControl.TryGet(option.GetName(), out value); - /// - /// Attempts to get a boolean config value from the UiControl section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(UiControlOption option, out bool value) => this.UiControl.TryGet(option.GetName(), out value); - /// - /// Attempts to get a uint config value from the UiControl section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(UiControlOption option, out uint value) => this.UiControl.TryGet(option.GetName(), out value); - /// - /// Attempts to get a float config value from the UiControl section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(UiControlOption option, out float value) => this.UiControl.TryGet(option.GetName(), out value); - /// - /// Attempts to get a string config value from the UiControl section. - /// - /// Option to get the value of. - /// The returned value of the config option. - /// A value representing the success. + /// public bool TryGet(UiControlOption option, out string value) => this.System.TryGet(option.GetName(), out value); - - /// - /// Set a boolean config option in the System config section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + + /// public void Set(SystemConfigOption option, bool value) => this.System.Set(option.GetName(), value); - /// - /// Set a unsigned integer config option in the System config section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + /// public void Set(SystemConfigOption option, uint value) => this.System.Set(option.GetName(), value); - /// - /// Set a float config option in the System config section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + /// public void Set(SystemConfigOption option, float value) => this.System.Set(option.GetName(), value); - /// - /// Set a string config option in the System config section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + /// public void Set(SystemConfigOption option, string value) => this.System.Set(option.GetName(), value); - /// - /// Set a boolean config option in the UiConfig section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + /// public void Set(UiConfigOption option, bool value) => this.UiConfig.Set(option.GetName(), value); - - /// - /// Set a unsigned integer config option in the UiConfig section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + + /// public void Set(UiConfigOption option, uint value) => this.UiConfig.Set(option.GetName(), value); - /// - /// Set a float config option in the UiConfig section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + /// public void Set(UiConfigOption option, float value) => this.UiConfig.Set(option.GetName(), value); - /// - /// Set a string config option in the UiConfig section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + /// public void Set(UiConfigOption option, string value) => this.UiConfig.Set(option.GetName(), value); - - /// - /// Set a boolean config option in the UiControl config section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + + /// public void Set(UiControlOption option, bool value) => this.UiControl.Set(option.GetName(), value); - - /// - /// Set a uint config option in the UiControl config section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + + /// public void Set(UiControlOption option, uint value) => this.UiControl.Set(option.GetName(), value); - - /// - /// Set a float config option in the UiControl config section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + + /// public void Set(UiControlOption option, float value) => this.UiControl.Set(option.GetName(), value); - - /// - /// Set a string config option in the UiControl config section. - /// Note: Not all config options will be be immediately reflected in the game. - /// - /// Name of the config option. - /// New value of the config option. - /// Throw if the config option is not found. - /// Thrown if the name of the config option is found, but the struct was not. + + /// public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value); } diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs new file mode 100644 index 000000000..bbff123c0 --- /dev/null +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -0,0 +1,242 @@ +using System.Diagnostics; + +using Dalamud.Game.Config; + +namespace Dalamud.Plugin.Services; + +/// +/// This class represents the game's configuration. +/// +public interface IGameConfig +{ + /// + /// Gets the collection of config options that persist between characters. + /// + public GameConfigSection System { get; } + + /// + /// Gets the collection of config options that are character specific. + /// + public GameConfigSection UiConfig { get; } + + /// + /// Gets the collection of config options that are control mode specific. (Mouse and Keyboard / Gamepad). + /// + public GameConfigSection UiControl { get; } + + /// + /// Attempts to get a boolean config value from the System section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(SystemConfigOption option, out bool value); + + /// + /// Attempts to get a uint config value from the System section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(SystemConfigOption option, out uint value); + + /// + /// Attempts to get a float config value from the System section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(SystemConfigOption option, out float value); + + /// + /// Attempts to get a string config value from the System section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(SystemConfigOption option, out string value); + + /// + /// Attempts to get a boolean config value from the UiConfig section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(UiConfigOption option, out bool value); + + /// + /// Attempts to get a uint config value from the UiConfig section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(UiConfigOption option, out uint value); + + /// + /// Attempts to get a float config value from the UiConfig section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(UiConfigOption option, out float value); + + /// + /// Attempts to get a string config value from the UiConfig section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(UiConfigOption option, out string value); + + /// + /// Attempts to get a boolean config value from the UiControl section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(UiControlOption option, out bool value); + + /// + /// Attempts to get a uint config value from the UiControl section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(UiControlOption option, out uint value); + + /// + /// Attempts to get a float config value from the UiControl section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(UiControlOption option, out float value); + + /// + /// Attempts to get a string config value from the UiControl section. + /// + /// Option to get the value of. + /// The returned value of the config option. + /// A value representing the success. + public bool TryGet(UiControlOption option, out string value); + + /// + /// Set a boolean config option in the System config section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(SystemConfigOption option, bool value); + + /// + /// Set a unsigned integer config option in the System config section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(SystemConfigOption option, uint value); + + /// + /// Set a float config option in the System config section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(SystemConfigOption option, float value); + + /// + /// Set a string config option in the System config section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(SystemConfigOption option, string value); + + /// + /// Set a boolean config option in the UiConfig section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(UiConfigOption option, bool value); + + /// + /// Set a unsigned integer config option in the UiConfig section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(UiConfigOption option, uint value); + + /// + /// Set a float config option in the UiConfig section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(UiConfigOption option, float value); + + /// + /// Set a string config option in the UiConfig section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(UiConfigOption option, string value); + + /// + /// Set a boolean config option in the UiControl config section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(UiControlOption option, bool value); + + /// + /// Set a uint config option in the UiControl config section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(UiControlOption option, uint value); + + /// + /// Set a float config option in the UiControl config section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(UiControlOption option, float value); + + /// + /// Set a string config option in the UiControl config section. + /// Note: Not all config options will be be immediately reflected in the game. + /// + /// Name of the config option. + /// New value of the config option. + /// Throw if the config option is not found. + /// Thrown if the name of the config option is found, but the struct was not. + public void Set(UiControlOption option, string value); +} From 518fc348e16386eb1636b40860d3909e330bb92d Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 21:03:47 -0700 Subject: [PATCH 19/81] Add IDutyState (#1275) --- Dalamud/Game/DutyState/DutyState.cs | 45 +++++++++------------------ Dalamud/Plugin/Services/IDutyState.cs | 36 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 Dalamud/Plugin/Services/IDutyState.cs diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 11eeff9f3..49fc874e3 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; namespace Dalamud.Game.DutyState; @@ -15,7 +16,10 @@ namespace Dalamud.Game.DutyState; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -public unsafe class DutyState : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public unsafe class DutyState : IDisposable, IServiceType, IDutyState { private readonly DutyStateAddressResolver address; private readonly Hook contentDirectorNetworkMessageHook; @@ -44,42 +48,24 @@ public unsafe class DutyState : IDisposable, IServiceType [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate byte SetupContentDirectNetworkMessageDelegate(IntPtr a1, IntPtr a2, ushort* a3); - /// - /// Event that gets fired when the duty starts. Triggers when the "Duty Start" - /// message displays, and on the remove of the ring at duty's spawn. - /// + /// public event EventHandler DutyStarted; - /// - /// Event that gets fired when everyone in the party dies and the screen fades to black. - /// + /// public event EventHandler DutyWiped; - - /// - /// Event that gets fired when the "Duty Recommence" message displays, - /// and on the remove the the ring at duty's spawn. - /// + + /// public event EventHandler DutyRecommenced; - - /// - /// Event that gets fired when the duty is completed successfully. - /// + + /// public event EventHandler DutyCompleted; - - /// - /// Gets a value indicating whether the current duty has been started. - /// + + /// public bool IsDutyStarted { get; private set; } - /// - /// Gets or sets a value indicating whether the current duty has been completed or not. - /// Prevents DutyStarted from triggering if combat is entered after receiving a duty complete network event. - /// private bool CompletedThisTerritory { get; set; } - /// - /// Dispose of managed and unmanaged resources. - /// + /// void IDisposable.Dispose() { this.contentDirectorNetworkMessageHook.Dispose(); @@ -171,9 +157,6 @@ public unsafe class DutyState : IDisposable, IServiceType else if (!this.IsBoundByDuty() && this.IsDutyStarted) { this.IsDutyStarted = false; - - // Could potentially add a call to DutyCompleted here since this - // should only be reached if we are actually no longer in a duty, and missed the network event. } } diff --git a/Dalamud/Plugin/Services/IDutyState.cs b/Dalamud/Plugin/Services/IDutyState.cs new file mode 100644 index 000000000..a2331364c --- /dev/null +++ b/Dalamud/Plugin/Services/IDutyState.cs @@ -0,0 +1,36 @@ +using System; + +namespace Dalamud.Plugin.Services; + +/// +/// This class represents the state of the currently occupied duty. +/// +public interface IDutyState +{ + /// + /// Event that gets fired when the duty starts. + /// Triggers when the "Duty Start" message displays, and on the removal of the ring at duty's spawn. + /// Does not trigger when loading into a duty that was in progress, or from loading in after a disconnect. + /// + public event EventHandler DutyStarted; + + /// + /// Event that gets fired when everyone in the party dies and the screen fades to black. + /// + public event EventHandler DutyWiped; + + /// + /// Event that gets fired when the "Duty Recommence" message displays, and on the removal of the ring at duty's spawn. + /// + public event EventHandler DutyRecommenced; + + /// + /// Event that gets fired when the duty is completed successfully. + /// + public event EventHandler DutyCompleted; + + /// + /// Gets a value indicating whether the current duty has been started. + /// + public bool IsDutyStarted { get; } +} From 3fa8968027fca50f0cda20ba72b63db0df93a3b8 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 21:07:17 -0700 Subject: [PATCH 20/81] Add IDtrBar (#1276) --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 19 ++++++++----------- Dalamud/Plugin/Services/IDtrBar.cs | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 Dalamud/Plugin/Services/IDtrBar.cs diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index d77b406f0..dd1e7aa30 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -6,6 +6,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Game.Text.SeStringHandling; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; using Serilog; @@ -18,7 +19,10 @@ namespace Dalamud.Game.Gui.Dtr; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed unsafe class DtrBar : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; @@ -44,14 +48,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType this.configuration.QueueSave(); } - /// - /// Get a DTR bar entry. - /// This allows you to add your own text, and users to sort it. - /// - /// A user-friendly name for sorting. - /// The text the entry shows. - /// The entry object used to update, hide and remove the entry. - /// Thrown when an entry with the specified title exists. + /// public DtrBarEntry Get(string title, SeString? text = null) { if (this.entries.Any(x => x.Title == title)) @@ -134,7 +131,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType }); } - private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR", 1).ToPointer(); + private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); private void Update(Framework unused) { @@ -293,7 +290,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType newTextNode->AtkResNode.NodeID = nodeId; newTextNode->AtkResNode.Type = NodeType.Text; - newTextNode->AtkResNode.Flags = (short)(NodeFlags.AnchorLeft | NodeFlags.AnchorTop); + newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop; newTextNode->AtkResNode.DrawFlags = 12; newTextNode->AtkResNode.SetWidth(22); newTextNode->AtkResNode.SetHeight(22); diff --git a/Dalamud/Plugin/Services/IDtrBar.cs b/Dalamud/Plugin/Services/IDtrBar.cs new file mode 100644 index 000000000..6c2b8ad1e --- /dev/null +++ b/Dalamud/Plugin/Services/IDtrBar.cs @@ -0,0 +1,22 @@ +using System; + +using Dalamud.Game.Gui.Dtr; +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Plugin.Services; + +/// +/// Class used to interface with the server info bar. +/// +public interface IDtrBar +{ + /// + /// Get a DTR bar entry. + /// This allows you to add your own text, and users to sort it. + /// + /// A user-friendly name for sorting. + /// The text the entry shows. + /// The entry object used to update, hide and remove the entry. + /// Thrown when an entry with the specified title exists. + public DtrBarEntry Get(string title, SeString? text = null); +} From 2fe7cfb5ef6b6273cd3203b67664e3e3de791439 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 21:24:08 -0700 Subject: [PATCH 21/81] Add IsTargetable to GameObject (#1269) * Add IsTargetable * Fix style inconsistency --- Dalamud/Game/ClientState/Objects/Types/GameObject.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs index 62b69dcec..292430b27 100644 --- a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs +++ b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs @@ -144,6 +144,11 @@ public unsafe partial class GameObject /// public bool IsDead => this.Struct->IsDead(); + /// + /// Gets a value indicating whether the object is targetable. + /// + public bool IsTargetable => this.Struct->GetIsTargetable(); + /// /// Gets the position of this . /// From 8a6269c178bf9b26abf1a0124ef6c0a4b6008cbc Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 23:34:45 -0700 Subject: [PATCH 22/81] Add IGameGui (#1282) --- Dalamud/Game/Gui/GameGui.cs | 94 ++++++----------------- Dalamud/Plugin/Services/IGameGui.cs | 112 ++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 72 deletions(-) create mode 100644 Dalamud/Plugin/Services/IGameGui.cs diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 59c136416..0235bef5a 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -7,6 +7,7 @@ using Dalamud.Hooking; using Dalamud.Interface; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.String; @@ -28,7 +29,10 @@ namespace Dalamud.Game.Gui; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed unsafe class GameGui : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui { private readonly GameGuiAddressResolver address; @@ -112,43 +116,26 @@ public sealed unsafe class GameGui : IDisposable, IServiceType [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, byte unknownByte); - - /// - /// Event which is fired when the game UI hiding is toggled. - /// + + /// public event EventHandler UiHideToggled; - /// - /// Event that is fired when the currently hovered item changes. - /// + /// public event EventHandler HoveredItemChanged; - /// - /// Event that is fired when the currently hovered action changes. - /// + /// public event EventHandler HoveredActionChanged; - /// - /// Gets a value indicating whether the game UI is hidden. - /// + /// public bool GameUiHidden { get; private set; } - /// - /// Gets or sets the item ID that is currently hovered by the player. 0 when no item is hovered. - /// If > 1.000.000, subtract 1.000.000 and treat it as HQ. - /// + /// public ulong HoveredItem { get; set; } - /// - /// Gets the action ID that is current hovered by the player. 0 when no action is hovered. - /// + /// public HoveredAction HoveredAction { get; } = new HoveredAction(); - /// - /// Opens the in-game map with a flag on the location of the parameter. - /// - /// Link to the map to be opened. - /// True if there were no errors and it could open the map. + /// public bool OpenMapWithMapLink(MapLinkPayload mapLink) { var uiModule = this.GetUIModule(); @@ -178,22 +165,11 @@ public sealed unsafe class GameGui : IDisposable, IServiceType return this.openMapWithFlag(uiMapObjectPtr, mapLinkString); } - /// - /// Converts in-world coordinates to screen coordinates (upper left corner origin). - /// - /// Coordinates in the world. - /// Converted coordinates. - /// True if worldPos corresponds to a position in front of the camera and screenPos is in the viewport. + /// public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos) => this.WorldToScreen(worldPos, out screenPos, out var inView) && inView; - /// - /// Converts in-world coordinates to screen coordinates (upper left corner origin). - /// - /// Coordinates in the world. - /// Converted coordinates. - /// True if screenPos corresponds to a position inside the camera viewport. - /// True if worldPos corresponds to a position in front of the camera. + /// public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos, out bool inView) { // Get base object with matrices @@ -220,13 +196,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType return inFront; } - /// - /// Converts screen coordinates to in-world coordinates via raycasting. - /// - /// Screen coordinates. - /// Converted coordinates. - /// How far to search for a collision. - /// True if successful. On false, worldPos's contents are undefined. + /// public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f) { // The game is only visible in the main viewport, so if the cursor is outside @@ -290,10 +260,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType return isSuccess; } - /// - /// Gets a pointer to the game's UI module. - /// - /// IntPtr pointing to UI module. + /// public IntPtr GetUIModule() { var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); @@ -307,12 +274,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType return (IntPtr)uiModule; } - /// - /// Gets the pointer to the Addon with the given name and index. - /// - /// Name of addon to find. - /// Index of addon to find (1-indexed). - /// IntPtr.Zero if unable to find UI, otherwise IntPtr pointing to the start of the addon. + /// public IntPtr GetAddonByName(string name, int index = 1) { var atkStage = AtkStage.GetSingleton(); @@ -330,29 +292,17 @@ public sealed unsafe class GameGui : IDisposable, IServiceType return (IntPtr)addon; } - /// - /// Find the agent associated with an addon, if possible. - /// - /// The addon name. - /// A pointer to the agent interface. + /// public IntPtr FindAgentInterface(string addonName) { var addon = this.GetAddonByName(addonName); return this.FindAgentInterface(addon); } - /// - /// Find the agent associated with an addon, if possible. - /// - /// The addon address. - /// A pointer to the agent interface. + /// public IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon); - - /// - /// Find the agent associated with an addon, if possible. - /// - /// The addon address. - /// A pointer to the agent interface. + + /// public IntPtr FindAgentInterface(IntPtr addonPtr) { if (addonPtr == IntPtr.Zero) diff --git a/Dalamud/Plugin/Services/IGameGui.cs b/Dalamud/Plugin/Services/IGameGui.cs new file mode 100644 index 000000000..ddb0ec67c --- /dev/null +++ b/Dalamud/Plugin/Services/IGameGui.cs @@ -0,0 +1,112 @@ +using System; +using System.Numerics; + +using Dalamud.Game.Gui; +using Dalamud.Game.Text.SeStringHandling.Payloads; + +namespace Dalamud.Plugin.Services; + +/// +/// A class handling many aspects of the in-game UI. +/// +public unsafe interface IGameGui +{ + /// + /// Event which is fired when the game UI hiding is toggled. + /// + public event EventHandler UiHideToggled; + + /// + /// Event that is fired when the currently hovered item changes. + /// + public event EventHandler HoveredItemChanged; + + /// + /// Event that is fired when the currently hovered action changes. + /// + public event EventHandler HoveredActionChanged; + + /// + /// Gets a value indicating whether the game UI is hidden. + /// + public bool GameUiHidden { get; } + + /// + /// Gets or sets the item ID that is currently hovered by the player. 0 when no item is hovered. + /// If > 1.000.000, subtract 1.000.000 and treat it as HQ. + /// + public ulong HoveredItem { get; set; } + + /// + /// Gets the action ID that is current hovered by the player. 0 when no action is hovered. + /// + public HoveredAction HoveredAction { get; } + + /// + /// Opens the in-game map with a flag on the location of the parameter. + /// + /// Link to the map to be opened. + /// True if there were no errors and it could open the map. + public bool OpenMapWithMapLink(MapLinkPayload mapLink); + + /// + /// Converts in-world coordinates to screen coordinates (upper left corner origin). + /// + /// Coordinates in the world. + /// Converted coordinates. + /// True if worldPos corresponds to a position in front of the camera and screenPos is in the viewport. + public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos); + + /// + /// Converts in-world coordinates to screen coordinates (upper left corner origin). + /// + /// Coordinates in the world. + /// Converted coordinates. + /// True if screenPos corresponds to a position inside the camera viewport. + /// True if worldPos corresponds to a position in front of the camera. + public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos, out bool inView); + + /// + /// Converts screen coordinates to in-world coordinates via raycasting. + /// + /// Screen coordinates. + /// Converted coordinates. + /// How far to search for a collision. + /// True if successful. On false, worldPos's contents are undefined. + public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f); + + /// + /// Gets a pointer to the game's UI module. + /// + /// IntPtr pointing to UI module. + public nint GetUIModule(); + + /// + /// Gets the pointer to the Addon with the given name and index. + /// + /// Name of addon to find. + /// Index of addon to find (1-indexed). + /// nint.Zero if unable to find UI, otherwise nint pointing to the start of the addon. + public nint GetAddonByName(string name, int index = 1); + + /// + /// Find the agent associated with an addon, if possible. + /// + /// The addon name. + /// A pointer to the agent interface. + public nint FindAgentInterface(string addonName); + + /// + /// Find the agent associated with an addon, if possible. + /// + /// The addon address. + /// A pointer to the agent interface. + public nint FindAgentInterface(void* addon); + + /// + /// Find the agent associated with an addon, if possible. + /// + /// The addon address. + /// A pointer to the agent interface. + public IntPtr FindAgentInterface(IntPtr addonPtr); +} From 0ac5c240f61d4e1bf797e14aa0c66ddda00140b3 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 23:36:58 -0700 Subject: [PATCH 23/81] Add IGameLifecycle (#1287) --- Dalamud/Game/GameLifecycle.cs | 18 ++++++++--------- Dalamud/Plugin/Services/IGameLifecycle.cs | 24 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 Dalamud/Plugin/Services/IGameLifecycle.cs diff --git a/Dalamud/Game/GameLifecycle.cs b/Dalamud/Game/GameLifecycle.cs index 3a6733512..5c1acc989 100644 --- a/Dalamud/Game/GameLifecycle.cs +++ b/Dalamud/Game/GameLifecycle.cs @@ -2,6 +2,7 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; namespace Dalamud.Game; @@ -11,7 +12,10 @@ namespace Dalamud.Game; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public class GameLifecycle : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public class GameLifecycle : IServiceType, IGameLifecycle { private readonly CancellationTokenSource dalamudUnloadCts = new(); private readonly CancellationTokenSource gameShutdownCts = new(); @@ -26,19 +30,13 @@ public class GameLifecycle : IServiceType { } - /// - /// Gets a token that is cancelled when Dalamud is unloading. - /// + /// public CancellationToken DalamudUnloadingToken => this.dalamudUnloadCts.Token; - /// - /// Gets a token that is cancelled when the game is shutting down. - /// + /// public CancellationToken GameShuttingDownToken => this.gameShutdownCts.Token; - /// - /// Gets a token that is cancelled when a character is logging out. - /// + /// public CancellationToken LogoutToken => this.logoutCts.Token; /// diff --git a/Dalamud/Plugin/Services/IGameLifecycle.cs b/Dalamud/Plugin/Services/IGameLifecycle.cs new file mode 100644 index 000000000..caa64ed23 --- /dev/null +++ b/Dalamud/Plugin/Services/IGameLifecycle.cs @@ -0,0 +1,24 @@ +using System.Threading; + +namespace Dalamud.Plugin.Services; + +/// +/// Class offering cancellation tokens for common gameplay events. +/// +public interface IGameLifecycle +{ + /// + /// Gets a token that is cancelled when Dalamud is unloading. + /// + public CancellationToken DalamudUnloadingToken { get; } + + /// + /// Gets a token that is cancelled when the game is shutting down. + /// + public CancellationToken GameShuttingDownToken { get; } + + /// + /// Gets a token that is cancelled when a character is logging out. + /// + public CancellationToken LogoutToken { get; } +} From 1346daccf8a890e3b6199729155a49bc9d68fd56 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 23:45:00 -0700 Subject: [PATCH 24/81] Add ILibcFunction (#1283) --- Dalamud/Game/Libc/LibcFunction.cs | 23 ++++++++------------- Dalamud/Plugin/Services/ILibcFunction.cs | 26 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 Dalamud/Plugin/Services/ILibcFunction.cs diff --git a/Dalamud/Game/Libc/LibcFunction.cs b/Dalamud/Game/Libc/LibcFunction.cs index 4c58376f2..7dfc26b3b 100644 --- a/Dalamud/Game/Libc/LibcFunction.cs +++ b/Dalamud/Game/Libc/LibcFunction.cs @@ -4,6 +4,7 @@ using System.Text; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; namespace Dalamud.Game.Libc; @@ -13,7 +14,10 @@ namespace Dalamud.Game.Libc; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class LibcFunction : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed class LibcFunction : IServiceType, ILibcFunction { private readonly LibcFunctionAddressResolver address; private readonly StdStringFromCStringDelegate stdStringCtorCString; @@ -37,11 +41,7 @@ public sealed class LibcFunction : IServiceType [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr StdStringDeallocateDelegate(IntPtr address); - /// - /// Create a new string from the given bytes. - /// - /// The bytes to convert. - /// An owned std string object. + /// public OwnedStdString NewString(byte[] content) { // While 0x70 bytes in the memory should be enough in DX11 version, @@ -56,14 +56,9 @@ public sealed class LibcFunction : IServiceType return new OwnedStdString(pReallocString, this.DeallocateStdString); } - - /// - /// Create a new string form the given bytes. - /// - /// The bytes to convert. - /// A non-default encoding. - /// An owned std string object. - public OwnedStdString NewString(string content, Encoding encoding = null) + + /// + public OwnedStdString NewString(string content, Encoding? encoding = null) { encoding ??= Encoding.UTF8; diff --git a/Dalamud/Plugin/Services/ILibcFunction.cs b/Dalamud/Plugin/Services/ILibcFunction.cs new file mode 100644 index 000000000..bebd62936 --- /dev/null +++ b/Dalamud/Plugin/Services/ILibcFunction.cs @@ -0,0 +1,26 @@ +using System.Text; + +using Dalamud.Game.Libc; + +namespace Dalamud.Plugin.Services; + +/// +/// This class handles creating cstrings utilizing native game methods. +/// +public interface ILibcFunction +{ + /// + /// Create a new string from the given bytes. + /// + /// The bytes to convert. + /// An owned std string object. + public OwnedStdString NewString(byte[] content); + + /// + /// Create a new string form the given bytes. + /// + /// The bytes to convert. + /// A non-default encoding. + /// An owned std string object. + public OwnedStdString NewString(string content, Encoding? encoding = null); +} From 6f80b4c8a9fdfa99e2d5d1fce142827c15a77fd6 Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sat, 24 Jun 2023 23:45:58 -0700 Subject: [PATCH 25/81] add workflow to rollup changes from master -> v9 (#1268) --- .github/workflows/rollup.yml | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/rollup.yml diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml new file mode 100644 index 000000000..0aea7a807 --- /dev/null +++ b/.github/workflows/rollup.yml @@ -0,0 +1,48 @@ +name: Rollup changes to next version +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + strategy: + matrix: + branches: + - v9 + + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + ref: ${{ matrix.branches }} + token: ${{ secrets.UPDATE_PAT }} + - name: Create update branch + run: git checkout -b ${{ matrix.branches }}/rollup + - name: Initialize mandatory git config + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email noreply@github.com + git config --global pull.rebase false + - name: Update submodule + run: | + git fetch + git merge -s recursive -X ours origin/master + git push origin ${{ matrix.branches }}/rollup --force + - name: Create PR + run: | + echo ${{ secrets.UPDATE_PAT }} | gh auth login --with-token + prNumber=$(gh pr list --base ${{ matrix.branches }} --head ${{ matrix.branches }}/rollup --state open --json number --template "{{range .}}{{.number}}{{end}}") + if [ -z "$prNumber" ]; then + echo "No PR found, creating one" + gh pr create --head ${{ matrix.branches }}/rollup --title "[${{ matrix.branches }}] Rollup changes from master" --body "" --base ${{ matrix.branches }} + else + echo "PR already exists, ignoring" + fi From 072af41bcde19171fc5321f3244a8a7e2e716c88 Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sat, 24 Jun 2023 23:46:28 -0700 Subject: [PATCH 26/81] set EnableWindowsTargeting when building on non-Windows hosts (#1255) --- build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index eca8e308c..a4c346c80 100644 --- a/build.sh +++ b/build.sh @@ -58,5 +58,5 @@ fi echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" -"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet -"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false /p:EnableWindowsTargeting=true -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- /p:EnableWindowsTargeting=true "$@" From b68773bb1c200b924734193fcb54ded3ff3fdbe3 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 23:48:06 -0700 Subject: [PATCH 27/81] Add ISigScanner (#1288) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- Dalamud/Game/SigScanner.cs | 105 +++++------------ Dalamud/Plugin/Services/ISigScanner.cs | 150 +++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 79 deletions(-) create mode 100644 Dalamud/Plugin/Services/ISigScanner.cs diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index b19024098..b5fe0b5b3 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -15,12 +15,17 @@ using Serilog; namespace Dalamud.Game; +// TODO(v9): There are static functions here that we can't keep due to interfaces + /// /// A SigScanner facilitates searching for memory signatures in a given ProcessModule. /// [PluginInterface] [InterfaceVersion("1.0")] -public class SigScanner : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public class SigScanner : IDisposable, IServiceType, ISigScanner { private readonly FileInfo? cacheFile; @@ -65,69 +70,43 @@ public class SigScanner : IDisposable, IServiceType this.Load(); } - /// - /// Gets a value indicating whether or not the search on this module is performed on a copy. - /// + /// public bool IsCopy { get; } - /// - /// Gets a value indicating whether or not the ProcessModule is 32-bit. - /// + /// public bool Is32BitProcess { get; } - /// - /// Gets the base address of the search area. When copied, this will be the address of the copy. - /// + /// public IntPtr SearchBase => this.IsCopy ? this.moduleCopyPtr : this.Module.BaseAddress; - /// - /// Gets the base address of the .text section search area. - /// + /// public IntPtr TextSectionBase => new(this.SearchBase.ToInt64() + this.TextSectionOffset); - /// - /// Gets the offset of the .text section from the base of the module. - /// + /// public long TextSectionOffset { get; private set; } - /// - /// Gets the size of the text section. - /// + /// public int TextSectionSize { get; private set; } - /// - /// Gets the base address of the .data section search area. - /// + /// public IntPtr DataSectionBase => new(this.SearchBase.ToInt64() + this.DataSectionOffset); - /// - /// Gets the offset of the .data section from the base of the module. - /// + /// public long DataSectionOffset { get; private set; } - /// - /// Gets the size of the .data section. - /// + /// public int DataSectionSize { get; private set; } - /// - /// Gets the base address of the .rdata section search area. - /// + /// public IntPtr RDataSectionBase => new(this.SearchBase.ToInt64() + this.RDataSectionOffset); - /// - /// Gets the offset of the .rdata section from the base of the module. - /// + /// public long RDataSectionOffset { get; private set; } - /// - /// Gets the size of the .rdata section. - /// + /// public int RDataSectionSize { get; private set; } - /// - /// Gets the ProcessModule on which the search is performed. - /// + /// public ProcessModule Module { get; } private IntPtr TextSectionTop => this.TextSectionBase + this.TextSectionSize; @@ -229,11 +208,7 @@ public class SigScanner : IDisposable, IServiceType } } - /// - /// Scan for a byte signature in the .data section. - /// - /// The signature. - /// The real offset of the found signature. + /// public IntPtr ScanData(string signature) { var scanRet = Scan(this.DataSectionBase, this.DataSectionSize, signature); @@ -244,12 +219,7 @@ public class SigScanner : IDisposable, IServiceType return scanRet; } - /// - /// Try scanning for a byte signature in the .data section. - /// - /// The signature. - /// The real offset of the signature, if found. - /// true if the signature was found. + /// public bool TryScanData(string signature, out IntPtr result) { try @@ -264,11 +234,7 @@ public class SigScanner : IDisposable, IServiceType } } - /// - /// Scan for a byte signature in the whole module search area. - /// - /// The signature. - /// The real offset of the found signature. + /// public IntPtr ScanModule(string signature) { var scanRet = Scan(this.SearchBase, this.Module.ModuleMemorySize, signature); @@ -279,12 +245,7 @@ public class SigScanner : IDisposable, IServiceType return scanRet; } - /// - /// Try scanning for a byte signature in the whole module search area. - /// - /// The signature. - /// The real offset of the signature, if found. - /// true if the signature was found. + /// public bool TryScanModule(string signature, out IntPtr result) { try @@ -299,23 +260,14 @@ public class SigScanner : IDisposable, IServiceType } } - /// - /// Resolve a RVA address. - /// - /// The address of the next instruction. - /// The relative offset. - /// The calculated offset. + /// public IntPtr ResolveRelativeAddress(IntPtr nextInstAddr, int relOffset) { if (this.Is32BitProcess) throw new NotSupportedException("32 bit is not supported."); return nextInstAddr + relOffset; } - /// - /// Scan for a byte signature in the .text section. - /// - /// The signature. - /// The real offset of the found signature. + /// public IntPtr ScanText(string signature) { if (this.textCache != null) @@ -347,12 +299,7 @@ public class SigScanner : IDisposable, IServiceType return scanRet; } - /// - /// Try scanning for a byte signature in the .text section. - /// - /// The signature. - /// The real offset of the signature, if found. - /// true if the signature was found. + /// public bool TryScanText(string signature, out IntPtr result) { try diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs new file mode 100644 index 000000000..c3bb7d6c1 --- /dev/null +++ b/Dalamud/Plugin/Services/ISigScanner.cs @@ -0,0 +1,150 @@ +using System; +using System.Diagnostics; + +namespace Dalamud.Game; + +/// +/// A SigScanner facilitates searching for memory signatures in a given ProcessModule. +/// +public interface ISigScanner +{ + /// + /// Gets a value indicating whether or not the search on this module is performed on a copy. + /// + public bool IsCopy { get; } + + /// + /// Gets a value indicating whether or not the ProcessModule is 32-bit. + /// + public bool Is32BitProcess { get; } + + /// + /// Gets the base address of the search area. When copied, this will be the address of the copy. + /// + public IntPtr SearchBase { get; } + + /// + /// Gets the base address of the .text section search area. + /// + public IntPtr TextSectionBase { get; } + + /// + /// Gets the offset of the .text section from the base of the module. + /// + public long TextSectionOffset { get; } + + /// + /// Gets the size of the text section. + /// + public int TextSectionSize { get; } + + /// + /// Gets the base address of the .data section search area. + /// + public IntPtr DataSectionBase { get; } + + /// + /// Gets the offset of the .data section from the base of the module. + /// + public long DataSectionOffset { get; } + + /// + /// Gets the size of the .data section. + /// + public int DataSectionSize { get; } + + /// + /// Gets the base address of the .rdata section search area. + /// + public IntPtr RDataSectionBase { get; } + + /// + /// Gets the offset of the .rdata section from the base of the module. + /// + public long RDataSectionOffset { get; } + + /// + /// Gets the size of the .rdata section. + /// + public int RDataSectionSize { get; } + + /// + /// Gets the ProcessModule on which the search is performed. + /// + public ProcessModule Module { get; } + + /// + /// Scan for a .data address using a .text function. + /// This is intended to be used with IDA sigs. + /// Place your cursor on the line calling a static address, and create and IDA sig. + /// The signature and offset should not break through instruction boundaries. + /// + /// The signature of the function using the data. + /// The offset from function start of the instruction using the data. + /// An IntPtr to the static memory location. + public nint GetStaticAddressFromSig(string signature, int offset = 0); + + /// + /// Try scanning for a .data address using a .text function. + /// This is intended to be used with IDA sigs. + /// Place your cursor on the line calling a static address, and create and IDA sig. + /// + /// The signature of the function using the data. + /// An IntPtr to the static memory location, if found. + /// The offset from function start of the instruction using the data. + /// true if the signature was found. + public bool TryGetStaticAddressFromSig(string signature, out nint result, int offset = 0); + + /// + /// Scan for a byte signature in the .data section. + /// + /// The signature. + /// The real offset of the found signature. + public nint ScanData(string signature); + + /// + /// Try scanning for a byte signature in the .data section. + /// + /// The signature. + /// The real offset of the signature, if found. + /// true if the signature was found. + public bool TryScanData(string signature, out nint result); + + /// + /// Scan for a byte signature in the whole module search area. + /// + /// The signature. + /// The real offset of the found signature. + public nint ScanModule(string signature); + + /// + /// Try scanning for a byte signature in the whole module search area. + /// + /// The signature. + /// The real offset of the signature, if found. + /// true if the signature was found. + public bool TryScanModule(string signature, out nint result); + + /// + /// Resolve a RVA address. + /// + /// The address of the next instruction. + /// The relative offset. + /// The calculated offset. + public nint ResolveRelativeAddress(nint nextInstAddr, int relOffset); + + /// + /// Scan for a byte signature in the .text section. + /// + /// The signature. + /// The real offset of the found signature. + public nint ScanText(string signature); + + /// + /// Try scanning for a byte signature in the .text section. + /// + /// The signature. + /// The real offset of the signature, if found. + /// true if the signature was found. + public bool TryScanText(string signature, out nint result); +} From 8f971934f3ce0e59e0aaf98f00cfe8d1e9ca1613 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sun, 25 Jun 2023 08:54:56 +0200 Subject: [PATCH 28/81] ci: change invalid branch name for rollup --- .github/workflows/rollup.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml index 0aea7a807..a4f74529a 100644 --- a/.github/workflows/rollup.yml +++ b/.github/workflows/rollup.yml @@ -1,4 +1,4 @@ -name: Rollup changes to next version +name: Rollup changes to next version on: push: branches: @@ -25,7 +25,7 @@ jobs: ref: ${{ matrix.branches }} token: ${{ secrets.UPDATE_PAT }} - name: Create update branch - run: git checkout -b ${{ matrix.branches }}/rollup + run: git checkout -b ${{ matrix.branches }}-rollup - name: Initialize mandatory git config run: | git config --global user.name "github-actions[bot]" @@ -35,14 +35,14 @@ jobs: run: | git fetch git merge -s recursive -X ours origin/master - git push origin ${{ matrix.branches }}/rollup --force + git push origin ${{ matrix.branches }}-rollup --force - name: Create PR run: | echo ${{ secrets.UPDATE_PAT }} | gh auth login --with-token - prNumber=$(gh pr list --base ${{ matrix.branches }} --head ${{ matrix.branches }}/rollup --state open --json number --template "{{range .}}{{.number}}{{end}}") + prNumber=$(gh pr list --base ${{ matrix.branches }} --head ${{ matrix.branches }}-rollup --state open --json number --template "{{range .}}{{.number}}{{end}}") if [ -z "$prNumber" ]; then echo "No PR found, creating one" - gh pr create --head ${{ matrix.branches }}/rollup --title "[${{ matrix.branches }}] Rollup changes from master" --body "" --base ${{ matrix.branches }} + gh pr create --head ${{ matrix.branches }}-rollup --title "[${{ matrix.branches }}] Rollup changes from master" --body "" --base ${{ matrix.branches }} else echo "PR already exists, ignoring" fi From fe46fd33dc0d74e54d445c6ae44bd9804c078d89 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 23:55:18 -0700 Subject: [PATCH 29/81] Add ITargetManager (#1277) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Game/ClientState/Objects/TargetManager.cs | 44 +++++++++++-------- Dalamud/Plugin/Services/ITargetManager.cs | 44 +++++++++++++++++++ 2 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 Dalamud/Plugin/Services/ITargetManager.cs diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index ccd89e6a3..c4c1e3822 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -12,7 +12,10 @@ namespace Dalamud.Game.ClientState.Objects; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed unsafe class TargetManager : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed unsafe class TargetManager : IServiceType, ITargetManager { [ServiceManager.ServiceDependency] private readonly ClientState clientState = Service.Get(); @@ -28,50 +31,38 @@ public sealed unsafe class TargetManager : IServiceType this.address = this.clientState.AddressResolver; } - /// - /// Gets the address of the target manager. - /// + /// public IntPtr Address => this.address.TargetManager; - /// - /// Gets or sets the current target. - /// + /// public GameObject? Target { get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target); set => this.SetTarget(value); } - /// - /// Gets or sets the mouseover target. - /// + /// public GameObject? MouseOverTarget { get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget); set => this.SetMouseOverTarget(value); } - /// - /// Gets or sets the focus target. - /// + /// public GameObject? FocusTarget { get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget); set => this.SetFocusTarget(value); } - /// - /// Gets or sets the previous target. - /// + /// public GameObject? PreviousTarget { get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget); set => this.SetPreviousTarget(value); } - /// - /// Gets or sets the soft target. - /// + /// public GameObject? SoftTarget { get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget); @@ -84,84 +75,99 @@ public sealed unsafe class TargetManager : IServiceType /// Sets the current target. /// /// Actor to target. + [Obsolete("Use Target Property", false)] public void SetTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero); /// /// Sets the mouseover target. /// /// Actor to target. + [Obsolete("Use MouseOverTarget Property", false)] public void SetMouseOverTarget(GameObject? actor) => this.SetMouseOverTarget(actor?.Address ?? IntPtr.Zero); /// /// Sets the focus target. /// /// Actor to target. + [Obsolete("Use FocusTarget Property", false)] public void SetFocusTarget(GameObject? actor) => this.SetFocusTarget(actor?.Address ?? IntPtr.Zero); /// /// Sets the previous target. /// /// Actor to target. + [Obsolete("Use PreviousTarget Property", false)] public void SetPreviousTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero); /// /// Sets the soft target. /// /// Actor to target. + [Obsolete("Use SoftTarget Property", false)] public void SetSoftTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero); /// /// Sets the current target. /// /// Actor (address) to target. + [Obsolete("Use Target Property", false)] public void SetTarget(IntPtr actorAddress) => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; /// /// Sets the mouseover target. /// /// Actor (address) to target. + [Obsolete("Use MouseOverTarget Property", false)] public void SetMouseOverTarget(IntPtr actorAddress) => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; /// /// Sets the focus target. /// /// Actor (address) to target. + [Obsolete("Use FocusTarget Property", false)] public void SetFocusTarget(IntPtr actorAddress) => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; /// /// Sets the previous target. /// /// Actor (address) to target. + [Obsolete("Use PreviousTarget Property", false)] public void SetPreviousTarget(IntPtr actorAddress) => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; /// /// Sets the soft target. /// /// Actor (address) to target. + [Obsolete("Use SoftTarget Property", false)] public void SetSoftTarget(IntPtr actorAddress) => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; /// /// Clears the current target. /// + [Obsolete("Use Target Property", false)] public void ClearTarget() => this.SetTarget(IntPtr.Zero); /// /// Clears the mouseover target. /// + [Obsolete("Use MouseOverTarget Property", false)] public void ClearMouseOverTarget() => this.SetMouseOverTarget(IntPtr.Zero); /// /// Clears the focus target. /// + [Obsolete("Use FocusTarget Property", false)] public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero); /// /// Clears the previous target. /// + [Obsolete("Use PreviousTarget Property", false)] public void ClearPreviousTarget() => this.SetPreviousTarget(IntPtr.Zero); /// /// Clears the soft target. /// + [Obsolete("Use SoftTarget Property", false)] public void ClearSoftTarget() => this.SetSoftTarget(IntPtr.Zero); } diff --git a/Dalamud/Plugin/Services/ITargetManager.cs b/Dalamud/Plugin/Services/ITargetManager.cs new file mode 100644 index 000000000..108b1ca03 --- /dev/null +++ b/Dalamud/Plugin/Services/ITargetManager.cs @@ -0,0 +1,44 @@ +using Dalamud.Game.ClientState.Objects.Types; + +namespace Dalamud.Game.ClientState.Objects; + +/// +/// Get and set various kinds of targets for the player. +/// +public interface ITargetManager +{ + /// + /// Gets the address of the target manager. + /// + public nint Address { get; } + + /// + /// Gets or sets the current target. + /// Set to null to clear the target. + /// + public GameObject? Target { get; set; } + + /// + /// Gets or sets the mouseover target. + /// Set to null to clear the target. + /// + public GameObject? MouseOverTarget { get; set; } + + /// + /// Gets or sets the focus target. + /// Set to null to clear the target. + /// + public GameObject? FocusTarget { get; set; } + + /// + /// Gets or sets the previous target. + /// Set to null to clear the target. + /// + public GameObject? PreviousTarget { get; set; } + + /// + /// Gets or sets the soft target. + /// Set to null to clear the target. + /// + public GameObject? SoftTarget { get; set; } +} From 9f074abb0b9798565d2e75e42273d9dbfc7c9254 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sun, 25 Jun 2023 09:00:53 +0200 Subject: [PATCH 30/81] ci: only fetch origin master --- .github/workflows/rollup.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml index a4f74529a..2bf754100 100644 --- a/.github/workflows/rollup.yml +++ b/.github/workflows/rollup.yml @@ -33,7 +33,7 @@ jobs: git config --global pull.rebase false - name: Update submodule run: | - git fetch + git fetch origin master git merge -s recursive -X ours origin/master git push origin ${{ matrix.branches }}-rollup --force - name: Create PR From e8c411bd728f1a0ec985df7e3cefd03bd9f70c7d Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 26 Jun 2023 10:30:17 +0200 Subject: [PATCH 31/81] feat: add ImRaii.PopupModal() --- Dalamud.Interface/Raii/EndObjects.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dalamud.Interface/Raii/EndObjects.cs b/Dalamud.Interface/Raii/EndObjects.cs index 1edc9f518..2e5ad30d0 100644 --- a/Dalamud.Interface/Raii/EndObjects.cs +++ b/Dalamud.Interface/Raii/EndObjects.cs @@ -35,6 +35,15 @@ public static partial class ImRaii public static IEndObject Popup(string id, ImGuiWindowFlags flags) => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopup(id, flags)); + + public static IEndObject PopupModal(string id) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupModal(id)); + + public static IEndObject PopupModal(string id, ref bool open) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupModal(id, ref open)); + + public static IEndObject PopupModal(string id, ref bool open, ImGuiWindowFlags flags) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupModal(id, ref open, flags)); public static IEndObject ContextPopup(string id) => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextWindow(id)); From 1443c751f5cc839e144e493de7ea3a3c1ff97d46 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 26 Jun 2023 10:30:56 +0200 Subject: [PATCH 32/81] feat: add a tutorial for profiles --- .../Internal/DalamudConfiguration.cs | 5 + .../PluginInstaller/ProfileManagerWidget.cs | 97 ++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index cb33e7070..d192ab676 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -287,6 +287,11 @@ internal sealed class DalamudConfiguration : IServiceType /// public bool ProfilesEnabled { get; set; } = false; + /// + /// Gets or sets a value indicating whether or not the user has seen the profiles tutorial. + /// + public bool ProfilesHasSeenTutorial { get; set; } = false; + /// /// Gets or sets a value indicating whether or not Dalamud RMT filtering should be disabled. /// diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index b43d70e7d..dca81c2a7 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -4,6 +4,7 @@ using System.Numerics; using System.Threading.Tasks; using CheapLoc; +using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; @@ -48,10 +49,14 @@ internal class ProfileManagerWidget /// public void Draw() { + var tutorialTitle = Locs.TutorialTitle + "###collectionsTutorWindow"; + var tutorialId = ImGui.GetID(tutorialTitle); + this.DrawTutorial(tutorialTitle); + switch (this.mode) { case Mode.Overview: - this.DrawOverview(); + this.DrawOverview(tutorialId); break; case Mode.EditSingleProfile: @@ -70,7 +75,51 @@ internal class ProfileManagerWidget this.pickerSearch = string.Empty; } - private void DrawOverview() + private void DrawTutorial(string modalTitle) + { + var _ = true; + ImGui.SetNextWindowSize(new Vector2(450, 350), ImGuiCond.Appearing); + using (var popup = ImRaii.PopupModal(modalTitle, ref _)) + { + if (popup) + { + using var scrolling = ImRaii.Child("###scrolling", new Vector2(-1, -1)); + if (scrolling) + { + ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphOne); + ImGuiHelpers.ScaledDummy(3); + ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphTwo); + ImGuiHelpers.ScaledDummy(3); + ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphThree); + ImGuiHelpers.ScaledDummy(3); + ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphFour); + ImGuiHelpers.ScaledDummy(3); + ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommands); + ImGui.BulletText(Locs.TutorialCommandsEnable); + ImGui.BulletText(Locs.TutorialCommandsDisable); + ImGui.BulletText(Locs.TutorialCommandsToggle); + ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnd); + + var buttonWidth = 120f; + ImGui.SetCursorPosX((ImGui.GetWindowWidth() - buttonWidth) / 2); + if (ImGui.Button("OK", new Vector2(buttonWidth, 40))) + { + ImGui.CloseCurrentPopup(); + } + } + } + } + + var config = Service.Get(); + if (!config.ProfilesHasSeenTutorial) + { + ImGui.OpenPopup(modalTitle); + config.ProfilesHasSeenTutorial = true; + config.QueueSave(); + } + } + + private void DrawOverview(uint tutorialId) { var didAny = false; var profman = Service.Get(); @@ -101,6 +150,16 @@ internal class ProfileManagerWidget if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.ImportProfileHint); + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(5); + ImGui.SameLine(); + + if (ImGuiComponents.IconButton(FontAwesomeIcon.Question)) + ImGui.OpenPopup(tutorialId); + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(Locs.TutorialHint); ImGui.Separator(); ImGuiHelpers.ScaledDummy(5); @@ -488,6 +547,9 @@ internal class ProfileManagerWidget public static string ImportProfileHint => Loc.Localize("ProfileManagerImportProfile", "Import a shared collection from your clipboard"); + public static string TutorialHint => + Loc.Localize("ProfileManagerTutorialHint", "Learn more about collections"); + public static string AddProfile => Loc.Localize("ProfileManagerAddProfile", "Add a new collection"); public static string NotificationImportSuccess => @@ -504,5 +566,36 @@ internal class ProfileManagerWidget public static string NotInstalled(string name) => Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name); + + public static string TutorialTitle => + Loc.Localize("ProfileManagerTutorial", "About Collections"); + + public static string TutorialParagraphOne => + Loc.Localize("ProfileManagerTutorialParagraphOne", "Collections are shareable lists of plugins that can be enabled or disabled in the plugin installer or via chat commands.\nWhen a plugin is part of a collection, it will be enabled if the collection is enabled. If a plugin is part of multiple collections, it will be enabled if one or more collections it is a part of are enabled."); + + public static string TutorialParagraphTwo => + Loc.Localize("ProfileManagerTutorialParagraphTwo", "You can add plugins to collections by clicking the plus button when editing a collection on this screen, or by using the button with the toolbox icon on the \"Installed Plugins\" screen."); + + public static string TutorialParagraphThree => + Loc.Localize("ProfileManagerTutorialParagraphThree", "If a collection's \"Start on boot\" checkbox is ticked, the collection and the plugins within will be enabled every time the game starts up, even if it has been manually disabled in a prior session."); + + public static string TutorialParagraphFour => + Loc.Localize("ProfileManagerTutorialParagraphFour", "Individual plugins inside a collection also have a checkbox next to them. This indicates if a plugin is active within that collection - if the checkbox is not ticked, the plugin will not be enabled if that collection is active. Mind that it will still be enabled if the plugin is an active part of any other collection."); + + public static string TutorialCommands => + Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:"); + + public static string TutorialCommandsEnable => + Loc.Localize("ProfileManagerTutorialCommandsEnable", "/xlenableprofile \"Collection Name\" - Enable a collection"); + + public static string TutorialCommandsDisable => + Loc.Localize("ProfileManagerTutorialCommandsDisable", "/xldisableprofile \"Collection Name\" - Disable a collection"); + + public static string TutorialCommandsToggle => + Loc.Localize("ProfileManagerTutorialCommandsToggle", "/xltoggleprofile \"Collection Name\" - Toggle a collection's state"); + + public static string TutorialCommandsEnd => + Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order."); + } } From 22a764ed823a17ea2494ecfa47b72ab8beffd025 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 26 Jun 2023 17:39:11 +0200 Subject: [PATCH 33/81] fix: don't save manifests every time a plugin loads, note reason for save if save fails --- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 28 +++++++++++++------ .../Internal/Types/LocalPluginManifest.cs | 17 ++++++++++- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index ccb54cc99..82def29d0 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -865,7 +865,7 @@ internal partial class PluginManager : IDisposable, IServiceType // Document the url the plugin was installed from manifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty ? repoManifest.SourceRepo.PluginMasterUrl : LocalPluginManifest.FlagMainRepo; - manifest.Save(manifestFile); + manifest.Save(manifestFile, "installation"); Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})"); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 14ae4a0c0..42fb40f91 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -125,13 +125,15 @@ internal class LocalPlugin : IDisposable // Save the manifest to disk so there won't be any problems later. // We'll update the name property after it can be retrieved from the instance. - this.Manifest.Save(this.manifestFile); + this.Manifest.Save(this.manifestFile, "manifest was null"); } else { this.Manifest = manifest; } + var needsSaveDueToLegacyFiles = false; + // This converts from the ".disabled" file feature to the manifest instead. this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile); if (this.disabledFile.Exists) @@ -140,6 +142,8 @@ internal class LocalPlugin : IDisposable this.Manifest.Disabled = true; #pragma warning restore CS0618 this.disabledFile.Delete(); + + needsSaveDueToLegacyFiles = true; } // This converts from the ".testing" file feature to the manifest instead. @@ -148,13 +152,16 @@ internal class LocalPlugin : IDisposable { this.Manifest.Testing = true; this.testingFile.Delete(); + + needsSaveDueToLegacyFiles = true; } var pluginManager = Service.Get(); this.IsBanned = pluginManager.IsManifestBanned(this.Manifest) && !this.IsDev; this.BanReason = pluginManager.GetBanReason(this.Manifest); - this.SaveManifest(); + if (needsSaveDueToLegacyFiles) + this.SaveManifest("legacy"); } /// @@ -322,8 +329,11 @@ internal class LocalPlugin : IDisposable } // If we reload a plugin we don't want to delete it. Makes sense, right? - this.Manifest.ScheduledForDeletion = false; - this.SaveManifest(); + if (this.Manifest.ScheduledForDeletion) + { + this.Manifest.ScheduledForDeletion = false; + this.SaveManifest("Scheduled for deletion, but loading"); + } switch (this.State) { @@ -470,10 +480,10 @@ internal class LocalPlugin : IDisposable } // In-case the manifest name was a placeholder. Can occur when no manifest was included. - if (this.Manifest.Name.IsNullOrEmpty()) + if (this.Manifest.Name.IsNullOrEmpty() && !this.IsDev) { this.Manifest.Name = this.instance.Name; - this.Manifest.Save(this.manifestFile); + this.Manifest.Save(this.manifestFile, "manifest name null or empty"); } this.State = PluginState.Loaded; @@ -618,7 +628,7 @@ internal class LocalPlugin : IDisposable public void ScheduleDeletion(bool status = true) { this.Manifest.ScheduledForDeletion = status; - this.SaveManifest(); + this.SaveManifest("scheduling for deletion"); } /// @@ -633,7 +643,7 @@ internal class LocalPlugin : IDisposable this.Manifest = LocalPluginManifest.Load(manifest) ?? throw new Exception("Could not reload manifest."); // this.Manifest.Disabled = isDisabled; - this.SaveManifest(); + this.SaveManifest("dev reload"); } } @@ -665,5 +675,5 @@ internal class LocalPlugin : IDisposable config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); } - private void SaveManifest() => this.Manifest.Save(this.manifestFile); + private void SaveManifest(string reason) => this.Manifest.Save(this.manifestFile, reason); } diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs index e142f9cb0..bcb8cd9e7 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs @@ -3,6 +3,7 @@ using System.IO; using Dalamud.Utility; using Newtonsoft.Json; +using Serilog; namespace Dalamud.Plugin.Internal.Types; @@ -69,7 +70,21 @@ internal record LocalPluginManifest : PluginManifest /// Save a plugin manifest to file. /// /// Path to save at. - public void Save(FileInfo manifestFile) => Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented)); + /// The reason the manifest was saved. + public void Save(FileInfo manifestFile, string reason) + { + Log.Verbose("Saving manifest for '{PluginName}' because '{Reason}'", this.InternalName, reason); + + try + { + Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented)); + } + catch + { + Log.Error("Could not write out manifest for '{PluginName}' because '{Reason}'", this.InternalName, reason); + throw; + } + } /// /// Loads a plugin manifest from file. From 0cbf014165f712ca8fee853986f9aa4630b573f2 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 26 Jun 2023 17:39:38 +0200 Subject: [PATCH 34/81] build: 7.7.5.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 61e3d4233..e93c0c25a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.7.4.0 + 7.7.5.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From a88151de7f95dcf566314771dab1b44ada43b6be Mon Sep 17 00:00:00 2001 From: Cara Date: Tue, 27 Jun 2023 09:53:54 +0930 Subject: [PATCH 35/81] Update Config List (#1293) --- Dalamud/Game/Config/UiConfigOption.cs | 161 ++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/Dalamud/Game/Config/UiConfigOption.cs b/Dalamud/Game/Config/UiConfigOption.cs index 44be0d380..98fb4502d 100644 --- a/Dalamud/Game/Config/UiConfigOption.cs +++ b/Dalamud/Game/Config/UiConfigOption.cs @@ -3314,4 +3314,165 @@ public enum UiConfigOption /// [GameConfigOption("PvPFrontlinesGCFree", ConfigType.UInt)] PvPFrontlinesGCFree, + + /// + /// System option with the internal name PetMirageTypeFairy. + /// This option is a UInt. + /// + [GameConfigOption("PetMirageTypeFairy", ConfigType.UInt)] + PetMirageTypeFairy, + + /// + /// System option with the internal name ExHotbarChangeHotbar1IsFashion. + /// This option is a UInt. + /// + [GameConfigOption("ExHotbarChangeHotbar1IsFashion", ConfigType.UInt)] + ExHotbarChangeHotbar1IsFashion, + + /// + /// System option with the internal name HotbarCrossUseExDirectionAutoSwitch. + /// This option is a UInt. + /// + [GameConfigOption("HotbarCrossUseExDirectionAutoSwitch", ConfigType.UInt)] + HotbarCrossUseExDirectionAutoSwitch, + + /// + /// System option with the internal name NamePlateDispJobIcon. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateDispJobIcon", ConfigType.UInt)] + NamePlateDispJobIcon, + + /// + /// System option with the internal name NamePlateDispJobIconType. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateDispJobIconType", ConfigType.UInt)] + NamePlateDispJobIconType, + + /// + /// System option with the internal name NamePlateSetRoleColor. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateSetRoleColor", ConfigType.UInt)] + NamePlateSetRoleColor, + + /// + /// System option with the internal name NamePlateColorTank. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateColorTank", ConfigType.UInt)] + NamePlateColorTank, + + /// + /// System option with the internal name NamePlateEdgeTank. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateEdgeTank", ConfigType.UInt)] + NamePlateEdgeTank, + + /// + /// System option with the internal name NamePlateColorHealer. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateColorHealer", ConfigType.UInt)] + NamePlateColorHealer, + + /// + /// System option with the internal name NamePlateEdgeHealer. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateEdgeHealer", ConfigType.UInt)] + NamePlateEdgeHealer, + + /// + /// System option with the internal name NamePlateColorDps. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateColorDps", ConfigType.UInt)] + NamePlateColorDps, + + /// + /// System option with the internal name NamePlateEdgeDps. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateEdgeDps", ConfigType.UInt)] + NamePlateEdgeDps, + + /// + /// System option with the internal name NamePlateColorOtherClass. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateColorOtherClass", ConfigType.UInt)] + NamePlateColorOtherClass, + + /// + /// System option with the internal name NamePlateEdgeOtherClass. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateEdgeOtherClass", ConfigType.UInt)] + NamePlateEdgeOtherClass, + + /// + /// System option with the internal name NamePlateDispWorldTravel. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateDispWorldTravel", ConfigType.UInt)] + NamePlateDispWorldTravel, + + /// + /// System option with the internal name LogNameIconType. + /// This option is a UInt. + /// + [GameConfigOption("LogNameIconType", ConfigType.UInt)] + LogNameIconType, + + /// + /// System option with the internal name LogDispClassJobName. + /// This option is a UInt. + /// + [GameConfigOption("LogDispClassJobName", ConfigType.UInt)] + LogDispClassJobName, + + /// + /// System option with the internal name LogSetRoleColor. + /// This option is a UInt. + /// + [GameConfigOption("LogSetRoleColor", ConfigType.UInt)] + LogSetRoleColor, + + /// + /// System option with the internal name LogColorRoleTank. + /// This option is a UInt. + /// + [GameConfigOption("LogColorRoleTank", ConfigType.UInt)] + LogColorRoleTank, + + /// + /// System option with the internal name LogColorRoleHealer. + /// This option is a UInt. + /// + [GameConfigOption("LogColorRoleHealer", ConfigType.UInt)] + LogColorRoleHealer, + + /// + /// System option with the internal name LogColorRoleDPS. + /// This option is a UInt. + /// + [GameConfigOption("LogColorRoleDPS", ConfigType.UInt)] + LogColorRoleDPS, + + /// + /// System option with the internal name LogColorOtherClass. + /// This option is a UInt. + /// + [GameConfigOption("LogColorOtherClass", ConfigType.UInt)] + LogColorOtherClass, + + /// + /// System option with the internal name ItemInventryStoreEnd. + /// This option is a UInt. + /// + [GameConfigOption("ItemInventryStoreEnd", ConfigType.UInt)] + ItemInventryStoreEnd, } From 98221471d51f754648b2789962cace2c2b2140c1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Jun 2023 16:02:40 +0200 Subject: [PATCH 36/81] Add a simple network monitor to xldata. --- .../Internal/Windows/Data/DataKindEnum.cs | 7 +- .../Internal/Windows/Data/DataWindow.cs | 1 + .../Windows/Data/Widgets/DataShareWidget.cs | 2 +- .../Data/Widgets/NetworkMonitorWidget.cs | 172 ++++++++++++++++++ 4 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs index 99c6cb6e9..d7c4eb095 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs @@ -154,5 +154,10 @@ internal enum DataKind /// /// Data Share. /// - DataShare, + Data_Share, + + /// + /// Network Monitor. + /// + Network_Monitor, } diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index f392d3912..9d8dc1e93 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -48,6 +48,7 @@ internal class DataWindow : Window new DtrBarWidget(), new UIColorWidget(), new DataShareWidget(), + new NetworkMonitorWidget(), }; private readonly Dictionary dataKindNames = new(); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 6ec741fe8..ec7124042 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -9,7 +9,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class DataShareWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.DataShare; + public DataKind DataKind { get; init; } = DataKind.Data_Share; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs new file mode 100644 index 000000000..488f46fed --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +using Dalamud.Data; +using Dalamud.Game.Network; +using Dalamud.Interface.Raii; +using Dalamud.Memory; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget to display the current packets. +/// +internal class NetworkMonitorWidget : IDataWindowWidget +{ + private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId) + { + public readonly IReadOnlyList Data = Array.Empty(); + + public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr) + : this(opCode, direction, sourceActorId, targetActorId) + => this.Data = MemoryHelper.Read(dataPtr, widget.GetSizeFromOpCode(opCode), false); + } + + private readonly ConcurrentQueue packets = new(); + private readonly Dictionary opCodeDict = new(); + + private bool trackNetwork; + private int trackedPackets; + private Regex? trackedOpCodes; + private string filterString = string.Empty; + + /// Finalizes an instance of the class. + ~NetworkMonitorWidget() + { + if (this.trackNetwork) + { + this.trackNetwork = false; + var network = Service.GetNullable(); + if (network != null) + { + network.NetworkMessage -= this.OnNetworkMessage; + } + } + } + + /// + public DataKind DataKind { get; init; } = DataKind.Network_Monitor; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.trackNetwork = false; + this.trackedPackets = 20; + this.trackedOpCodes = null; + this.filterString = string.Empty; + this.packets.Clear(); + this.Ready = true; + var dataManager = Service.Get(); + foreach (var (name, code) in dataManager.ClientOpCodes.Concat(dataManager.ServerOpCodes)) + this.opCodeDict.TryAdd(code, (name, this.GetSizeFromName(name))); + } + + /// + public void Draw() + { + var network = Service.Get(); + if (ImGui.Checkbox("Track Network Packets", ref this.trackNetwork)) + { + if (this.trackNetwork) + { + network.NetworkMessage += this.OnNetworkMessage; + } + else + { + network.NetworkMessage -= this.OnNetworkMessage; + } + } + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2); + if (ImGui.DragInt("Stored Number of Packets", ref this.trackedPackets, 0.1f, 1, 512)) + { + this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512); + } + + this.DrawFilterInput(); + + ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "OpCode", "Source", "Target", "Data"); + } + + private void DrawNetworkPacket(NetworkPacketData data) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(data.Direction.ToString()); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(this.opCodeDict.TryGetValue(data.OpCode, out var pair) ? pair.Item1 : data.OpCode.ToString()); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"0x{data.SourceActorId:X}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"0x{data.TargetActorId:X}"); + + ImGui.TableNextColumn(); + if (data.Data.Count > 0) + { + ImGui.TextUnformatted(string.Join(" ", data.Data.Select(b => b.ToString("X2")))); + } + } + + private void DrawFilterInput() + { + var invalidRegEx = this.filterString.Length > 0 && this.trackedOpCodes == null; + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx); + using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (!ImGui.InputTextWithHint("##Filter", "Regex Filter OpCodes...", ref this.filterString, 256)) + { + return; + } + + if (this.filterString.Length == 0) + { + this.trackedOpCodes = null; + } + else + { + try + { + this.trackedOpCodes = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture); + } + catch + { + this.trackedOpCodes = null; + } + } + } + + private void OnNetworkMessage(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) + { + if (this.trackedOpCodes == null || this.trackedOpCodes.IsMatch(this.OpCodeToString(opCode))) + { + this.packets.Enqueue(new NetworkPacketData(this, opCode, direction, sourceActorId, targetActorId, dataPtr)); + while (this.packets.Count > this.trackedPackets) + { + this.packets.TryDequeue(out _); + } + } + } + + private int GetSizeFromOpCode(ushort opCode) + => this.opCodeDict.TryGetValue(opCode, out var pair) ? pair.Item2 : 0; + + /// Add known packet-name -> packet struct size associations here to copy the byte data for such packets. > + private int GetSizeFromName(string name) + => name switch + { + _ => 0, + }; + + /// The filter should find opCodes by number (decimal and hex) and name, if existing. + private string OpCodeToString(ushort opCode) + => this.opCodeDict.TryGetValue(opCode, out var pair) ? $"{pair.Item1}\0{opCode}\0{opCode:X}" : $"{opCode}\0{opCode:X}"; +} From 98bdec1e34b0f880faa95d4f81d79d7e24cc4043 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 28 Jun 2023 12:27:50 +0200 Subject: [PATCH 37/81] chore: only expose manifests as interfaces --- Dalamud/EntryPoint.cs | 2 +- .../Internal/Windows/PluginImageCache.cs | 28 ++--- .../DalamudChangelogManager.cs | 11 +- .../PluginInstaller/PluginChangelogEntry.cs | 2 +- .../PluginInstaller/PluginInstallerWindow.cs | 41 ++++--- .../PluginInstaller/ProfileManagerWidget.cs | 7 +- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- Dalamud/Plugin/Internal/PluginManager.cs | 6 +- .../Internal/Types/ILocalPluginManifest.cs | 19 +++ .../Plugin/Internal/Types/IPluginManifest.cs | 111 ++++++++++++++++++ Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 93 ++++++++------- .../Internal/Types/LocalPluginManifest.cs | 12 +- .../Plugin/Internal/Types/PluginManifest.cs | 98 ++++------------ Dalamud/Support/BugBait.cs | 2 +- Dalamud/Support/Troubleshooting.cs | 2 +- Dalamud/Utility/Util.cs | 2 +- 16 files changed, 257 insertions(+), 181 deletions(-) create mode 100644 Dalamud/Plugin/Internal/Types/ILocalPluginManifest.cs create mode 100644 Dalamud/Plugin/Internal/Types/IPluginManifest.cs diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 19b4f841c..1975505a8 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -263,7 +263,7 @@ public sealed class EntryPoint { pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n"; - if (plugin.Manifest.IsThirdParty) + if (plugin.IsThirdParty) supportText = string.Empty; } } diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 0aeb0722d..bcdf90fe4 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -232,7 +232,7 @@ internal class PluginImageCache : IDisposable, IServiceType /// If the plugin was third party sourced. /// Cached image textures, or an empty array. /// True if an entry exists, may be null if currently downloading. - public bool TryGetIcon(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, out TextureWrap? iconTexture) + public bool TryGetIcon(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out TextureWrap? iconTexture) { iconTexture = null; @@ -274,7 +274,7 @@ internal class PluginImageCache : IDisposable, IServiceType /// If the plugin was third party sourced. /// Cached image textures, or an empty array. /// True if the image array exists, may be empty if currently downloading. - public bool TryGetImages(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, out TextureWrap?[] imageTextures) + public bool TryGetImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out TextureWrap?[] imageTextures) { if (!this.pluginImagesMap.TryAdd(manifest.InternalName, null)) { @@ -307,7 +307,7 @@ internal class PluginImageCache : IDisposable, IServiceType byte[]? bytes, string name, string? loc, - PluginManifest manifest, + IPluginManifest manifest, int maxWidth, int maxHeight, bool requireSquare) @@ -491,7 +491,7 @@ internal class PluginImageCache : IDisposable, IServiceType Log.Debug("Plugin image loader has shutdown"); } - private async Task DownloadPluginIconAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, ulong requestedFrame) + private async Task DownloadPluginIconAsync(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame) { if (plugin is { IsDev: true }) { @@ -558,7 +558,7 @@ internal class PluginImageCache : IDisposable, IServiceType return icon; } - private async Task DownloadPluginImagesAsync(TextureWrap?[] pluginImages, LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, ulong requestedFrame) + private async Task DownloadPluginImagesAsync(TextureWrap?[] pluginImages, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame) { if (plugin is { IsDev: true }) { @@ -671,18 +671,15 @@ internal class PluginImageCache : IDisposable, IServiceType } } - private string? GetPluginIconUrl(PluginManifest manifest, bool isThirdParty, bool isTesting) + private string? GetPluginIconUrl(IPluginManifest manifest, bool isThirdParty, bool isTesting) { if (isThirdParty) return manifest.IconUrl; - if (manifest.IsDip17Plugin) - return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png"); - - return MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, "icon.png"); + return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png"); } - private List? GetPluginImageUrls(PluginManifest manifest, bool isThirdParty, bool isTesting) + private List? GetPluginImageUrls(IPluginManifest manifest, bool isThirdParty, bool isTesting) { if (isThirdParty) { @@ -698,14 +695,7 @@ internal class PluginImageCache : IDisposable, IServiceType var output = new List(); for (var i = 1; i <= 5; i++) { - if (manifest.IsDip17Plugin) - { - output.Add(MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, $"image{i}.png")); - } - else - { - output.Add(MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, $"image{i}.png")); - } + output.Add(MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, $"image{i}.png")); } return output; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs index 2dc182e9a..984732509 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs @@ -48,15 +48,12 @@ internal class DalamudChangelogManager foreach (var plugin in this.manager.InstalledPlugins) { - if (!plugin.Manifest.IsThirdParty) + if (!plugin.IsThirdParty) { - if (!plugin.Manifest.IsDip17Plugin) - continue; - var pluginChangelogs = await client.GetFromJsonAsync(string.Format( - PluginChangelogUrl, - plugin.Manifest.InternalName, - plugin.Manifest.Dip17Channel)); + PluginChangelogUrl, + plugin.Manifest.InternalName, + plugin.Manifest.Dip17Channel)); changelogs = changelogs.Concat(pluginChangelogs.Versions .Where(x => x.Dip17Track == plugin.Manifest.Dip17Channel) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs index 247e2d353..b4048536e 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs @@ -33,7 +33,7 @@ internal class PluginChangelogEntry : IChangelogEntry { this.Plugin = plugin; - this.Version = plugin.Manifest.EffectiveVersion.ToString(); + this.Version = plugin.EffectiveVersion.ToString(); this.Text = plugin.Manifest.Changelog ?? Loc.Localize("ChangelogNoText", "No changelog for this version."); this.Author = plugin.Manifest.Author; this.Date = DateTimeOffset.FromUnixTimeSeconds(this.Plugin.Manifest.LastUpdate).DateTime; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index ba249e051..80b4656b0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -88,7 +88,7 @@ internal class PluginInstallerWindow : Window, IDisposable private string feedbackModalBody = string.Empty; private string feedbackModalContact = string.Empty; private bool feedbackModalIncludeException = false; - private PluginManifest? feedbackPlugin = null; + private IPluginManifest? feedbackPlugin = null; private bool feedbackIsTesting = false; private int updatePluginCount = 0; @@ -1606,7 +1606,7 @@ internal class PluginInstallerWindow : Window, IDisposable return ready; } - private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, bool trouble, bool updateAvailable, bool isNew, bool installableOutdated, bool isOrphan, Action drawContextMenuAction, int index) + private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, bool trouble, bool updateAvailable, bool isNew, bool installableOutdated, bool isOrphan, Action drawContextMenuAction, int index) { ImGui.Separator(); @@ -1741,13 +1741,13 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.TextWrapped(Locs.PluginBody_Orphaned); ImGui.PopStyleColor(); } - else if (plugin is { IsDecommissioned: true } && !plugin.Manifest.IsThirdParty) + else if (plugin is { IsDecommissioned: true, IsThirdParty: false }) { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); ImGui.TextWrapped(Locs.PluginBody_NoServiceOfficial); ImGui.PopStyleColor(); } - else if (plugin is { IsDecommissioned: true } && plugin.Manifest.IsThirdParty) + else if (plugin is { IsDecommissioned: true, IsThirdParty: true }) { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); ImGui.TextWrapped(Locs.PluginBody_NoServiceThird); @@ -1808,7 +1808,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (log is PluginChangelogEntry pluginLog) { icon = this.imageCache.DefaultIcon; - var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.Manifest.IsThirdParty, out var cachedIconTex); + var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.IsThirdParty, out var cachedIconTex); if (hasIcon && cachedIconTex != null) { icon = cachedIconTex; @@ -2031,15 +2031,18 @@ internal class PluginInstallerWindow : Window, IDisposable } // Testing - if (plugin.Manifest.Testing) + if (plugin.IsTesting) { label += Locs.PluginTitleMod_TestingVersion; } + // TODO: check with the repos instead + /* if (plugin.Manifest.IsAvailableForTesting && configuration.DoPluginTest && testingOptIn == null) { label += Locs.PluginTitleMod_TestingAvailable; } + */ // Freshly installed if (showInstalled) @@ -2132,7 +2135,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}"); var hasChangelog = !plugin.Manifest.Changelog.IsNullOrEmpty(); - if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.Manifest.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index)) + if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index)) { if (!this.WasPluginSeen(plugin.Manifest.InternalName)) configuration.SeenPluginInternalName.Add(plugin.Manifest.InternalName); @@ -2154,12 +2157,12 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SameLine(); ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadText); - var isThirdParty = manifest.IsThirdParty; + var isThirdParty = plugin.IsThirdParty; var canFeedback = !isThirdParty && !plugin.IsDev && !plugin.IsOrphaned && plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel && - plugin.Manifest.AcceptsFeedback && + // plugin.Manifest.AcceptsFeedback && // TODO: check with the repos availablePluginUpdate == default; // Installed from @@ -2215,7 +2218,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.DrawUpdateSinglePluginButton(availablePluginUpdate); ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.Manifest.EffectiveVersion}"); + ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.EffectiveVersion}"); ImGuiHelpers.ScaledDummy(5); @@ -2226,7 +2229,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (hasChangelog) { - if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.Manifest.EffectiveVersion))) + if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion))) { this.DrawInstalledPluginChangelog(plugin.Manifest); ImGui.TreePop(); @@ -2252,7 +2255,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PopID(); } - private void DrawInstalledPluginChangelog(PluginManifest manifest) + private void DrawInstalledPluginChangelog(IPluginManifest manifest) { ImGuiHelpers.ScaledDummy(5); @@ -2265,7 +2268,7 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGui.Text("Changelog:"); ImGuiHelpers.ScaledDummy(2); - ImGuiHelpers.SafeTextWrapped(manifest.Changelog); + ImGuiHelpers.SafeTextWrapped(manifest.Changelog!); } ImGui.EndChild(); @@ -2363,7 +2366,7 @@ internal class PluginInstallerWindow : Window, IDisposable var isLoadedAndUnloadable = plugin.State == PluginState.Loaded || plugin.State == PluginState.DependencyResolutionFailed; - //StyleModelV1.DalamudStandard.Push(); + // StyleModelV1.DalamudStandard.Push(); var profileChooserPopupName = $"###pluginProfileChooser{plugin.Manifest.InternalName}"; if (ImGui.BeginPopup(profileChooserPopupName)) @@ -2526,7 +2529,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - //StyleModelV1.DalamudStandard.Pop(); + // StyleModelV1.DalamudStandard.Pop(); ImGui.SameLine(); ImGuiHelpers.ScaledDummy(15, 0); @@ -2621,7 +2624,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - private void DrawSendFeedbackButton(PluginManifest manifest, bool isTesting) + private void DrawSendFeedbackButton(IPluginManifest manifest, bool isTesting) { ImGui.SameLine(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Comment)) @@ -2796,7 +2799,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - private bool DrawPluginImages(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, int index) + private bool DrawPluginImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, int index) { var hasImages = this.imageCache.TryGetImages(plugin, manifest, isThirdParty, out var imageTextures); if (!hasImages || imageTextures.All(x => x == null)) @@ -2871,7 +2874,7 @@ internal class PluginInstallerWindow : Window, IDisposable return true; } - private bool IsManifestFiltered(PluginManifest manifest) + private bool IsManifestFiltered(IPluginManifest manifest) { var searchString = this.searchText.ToLowerInvariant(); var hasSearchString = !string.IsNullOrWhiteSpace(searchString); @@ -2889,7 +2892,7 @@ internal class PluginInstallerWindow : Window, IDisposable (manifest.Tags != null && manifest.Tags.Any(tag => tag.ToLowerInvariant().Contains(searchString)))); } - private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(PluginManifest? manifest) + private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(IPluginManifest? manifest) { if (manifest == null) return (false, default); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index dca81c2a7..db455b985 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -77,9 +77,9 @@ internal class ProfileManagerWidget private void DrawTutorial(string modalTitle) { - var _ = true; + var open = true; ImGui.SetNextWindowSize(new Vector2(450, 350), ImGuiCond.Appearing); - using (var popup = ImRaii.PopupModal(modalTitle, ref _)) + using (var popup = ImRaii.PopupModal(modalTitle, ref open)) { if (popup) { @@ -399,7 +399,7 @@ internal class ProfileManagerWidget if (pmPlugin != null) { - pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.Manifest.IsThirdParty, out var icon); + pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon); icon ??= pic.DefaultIcon; ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight)); @@ -596,6 +596,5 @@ internal class ProfileManagerWidget public static string TutorialCommandsEnd => Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order."); - } } diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 35b8bbbc7..1b9f065ab 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -220,7 +220,7 @@ public sealed class DalamudPluginInterface : IDisposable /// /// Gets a list of installed plugins along with their current state. /// - public IEnumerable InstalledPlugins => Service.Get().InstalledPlugins.Select(p => new InstalledPluginState(p.Name, p.Manifest.InternalName, p.IsLoaded, p.Manifest.EffectiveVersion)); + public IEnumerable InstalledPlugins => Service.Get().InstalledPlugins.Select(p => new InstalledPluginState(p.Name, p.Manifest.InternalName, p.IsLoaded, p.EffectiveVersion)); /// /// Opens the with the plugin name set as search target. diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 82def29d0..a7fec04e1 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -246,7 +246,7 @@ internal partial class PluginManager : IDisposable, IServiceType /// /// The manifest to test. /// Whether or not a testing version is available. - public static bool HasTestingVersion(PluginManifest manifest) + public static bool HasTestingVersion(IPluginManifest manifest) { var av = manifest.AssemblyVersion; var tv = manifest.TestingAssemblyVersion; @@ -316,7 +316,7 @@ internal partial class PluginManager : IDisposable, IServiceType /// /// Manifest to check. /// A value indicating whether testing should be used. - public bool HasTestingOptIn(PluginManifest manifest) + public bool HasTestingOptIn(IPluginManifest manifest) { return this.configuration.PluginTestingOptIns!.Any(x => x.InternalName == manifest.InternalName); } @@ -327,7 +327,7 @@ internal partial class PluginManager : IDisposable, IServiceType /// /// Manifest to check. /// A value indicating whether testing should be used. - public bool UseTesting(PluginManifest manifest) + public bool UseTesting(IPluginManifest manifest) { if (!this.configuration.DoPluginTest) return false; diff --git a/Dalamud/Plugin/Internal/Types/ILocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/ILocalPluginManifest.cs new file mode 100644 index 000000000..bbd40d2dd --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/ILocalPluginManifest.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Public interface for the local plugin manifest. +/// +public interface ILocalPluginManifest : IPluginManifest +{ + /// + /// Gets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was + /// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null + /// when installed from the main repo. + /// + public string InstalledFromUrl { get; } + + /// + /// Gets a value indicating whether the plugin should be deleted during the next cleanup. + /// + public bool ScheduledForDeletion { get; } +} diff --git a/Dalamud/Plugin/Internal/Types/IPluginManifest.cs b/Dalamud/Plugin/Internal/Types/IPluginManifest.cs new file mode 100644 index 000000000..90f2fb8bd --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/IPluginManifest.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; + +namespace Dalamud.Plugin.Internal.Types; + +/// +/// Public interface for the base plugin manifest. +/// +public interface IPluginManifest +{ + /// + /// Gets the internal name of the plugin, which should match the assembly name of the plugin. + /// + public string InternalName { get; } + + /// + /// Gets the public name of the plugin. + /// + public string Name { get; } + + /// + /// Gets a punchline of the plugins functions. + /// + public string? Punchline { get; } + + /// + /// Gets the author/s of the plugin. + /// + public string Author { get; } + + /// + /// Gets a value indicating whether the plugin can be unloaded asynchronously. + /// + public bool CanUnloadAsync { get; } + + /// + /// Gets the assembly version of the plugin. + /// + public Version AssemblyVersion { get; } + + /// + /// Gets the assembly version of the plugin's testing variant. + /// + public Version? TestingAssemblyVersion { get; } + + /// + /// Gets the DIP17 channel name. + /// + public string? Dip17Channel { get; } + + /// + /// Gets the last time this plugin was updated. + /// + public long LastUpdate { get; } + + /// + /// Gets a changelog, null if none exists. + /// + public string? Changelog { get; } + + /// + /// Gets a list of tags that apply to this plugin. + /// + public List? Tags { get; } + + /// + /// Gets the API level of this plugin. For the current API level, please see + /// for the currently used API level. + /// + public int DalamudApiLevel { get; } + + /// + /// Gets the number of downloads this plugin has. + /// + public long DownloadCount { get; } + + /// + /// Gets a value indicating whether the plugin supports profiles. + /// + public bool SupportsProfiles { get; } + + /// + /// Gets an URL to the website or source code of the plugin. + /// + public string? RepoUrl { get; } + + /// + /// Gets a description of the plugins functions. + /// + public string? Description { get; } + + /// + /// Gets a message that is shown to users when sending feedback. + /// + public string? FeedbackMessage { get; } + + /// + /// Gets a value indicating whether the plugin is only available for testing. + /// + public bool IsTestingExclusive { get; } + + /// + /// Gets a list of screenshot image URLs to show in the plugin installer. + /// + public List? ImageUrls { get; } + + /// + /// Gets an URL for the plugin's icon. + /// + public string? IconUrl { get; } +} diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 42fb40f91..d4caed0fd 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -11,7 +11,6 @@ using Dalamud.Game.Gui.Dtr; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.IoC.Internal; -using Dalamud.Logging; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Loader; @@ -39,6 +38,8 @@ internal class LocalPlugin : IDisposable private Type? pluginType; private IDalamudPlugin? instance; + private LocalPluginManifest manifest; + /// /// Initializes a new instance of the class. /// @@ -111,7 +112,7 @@ internal class LocalPlugin : IDisposable // If the parameter manifest was null if (manifest == null) { - this.Manifest = new LocalPluginManifest() + this.manifest = new LocalPluginManifest() { Author = "developer", Name = Path.GetFileNameWithoutExtension(this.DllFile.Name), @@ -125,11 +126,11 @@ internal class LocalPlugin : IDisposable // Save the manifest to disk so there won't be any problems later. // We'll update the name property after it can be retrieved from the instance. - this.Manifest.Save(this.manifestFile, "manifest was null"); + this.manifest.Save(this.manifestFile, "manifest was null"); } else { - this.Manifest = manifest; + this.manifest = manifest; } var needsSaveDueToLegacyFiles = false; @@ -139,7 +140,7 @@ internal class LocalPlugin : IDisposable if (this.disabledFile.Exists) { #pragma warning disable CS0618 - this.Manifest.Disabled = true; + this.manifest.Disabled = true; #pragma warning restore CS0618 this.disabledFile.Delete(); @@ -150,15 +151,15 @@ internal class LocalPlugin : IDisposable this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile); if (this.testingFile.Exists) { - this.Manifest.Testing = true; + this.manifest.Testing = true; this.testingFile.Delete(); needsSaveDueToLegacyFiles = true; } var pluginManager = Service.Get(); - this.IsBanned = pluginManager.IsManifestBanned(this.Manifest) && !this.IsDev; - this.BanReason = pluginManager.GetBanReason(this.Manifest); + this.IsBanned = pluginManager.IsManifestBanned(this.manifest) && !this.IsDev; + this.BanReason = pluginManager.GetBanReason(this.manifest); if (needsSaveDueToLegacyFiles) this.SaveManifest("legacy"); @@ -175,9 +176,9 @@ internal class LocalPlugin : IDisposable public FileInfo DllFile { get; } /// - /// Gets the plugin manifest, if one exists. + /// Gets the plugin manifest. /// - public LocalPluginManifest Manifest { get; private set; } + public ILocalPluginManifest Manifest => this.manifest; /// /// Gets or sets the current state of the plugin. @@ -193,12 +194,12 @@ internal class LocalPlugin : IDisposable /// /// Gets the plugin name from the manifest. /// - public string Name => this.Manifest.Name; + public string Name => this.manifest.Name; /// /// Gets the plugin internal name from the manifest. /// - public string InternalName => this.Manifest.InternalName; + public string InternalName => this.manifest.InternalName; /// /// Gets an optional reason, if the plugin is banned. @@ -220,23 +221,23 @@ internal class LocalPlugin : IDisposable /// INCLUDES the default profile. /// public bool IsWantedByAnyProfile => - Service.Get().GetWantStateAsync(this.Manifest.InternalName, false, false).GetAwaiter().GetResult(); + Service.Get().GetWantStateAsync(this.manifest.InternalName, false, false).GetAwaiter().GetResult(); /// /// Gets a value indicating whether this plugin's API level is out of date. /// - public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel; + public bool IsOutdated => this.manifest.DalamudApiLevel < PluginManager.DalamudApiLevel; /// /// Gets a value indicating whether the plugin is for testing use only. /// - public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing; + public bool IsTesting => this.manifest.IsTestingExclusive || this.manifest.Testing; /// /// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not. /// public bool IsOrphaned => !this.IsDev && - !this.Manifest.InstalledFromUrl.IsNullOrEmpty() && // TODO(api8): Remove this, all plugins will have a proper flag + !this.manifest.InstalledFromUrl.IsNullOrEmpty() && // TODO(api8): Remove this, all plugins will have a proper flag this.GetSourceRepository() == null; /// @@ -244,7 +245,7 @@ internal class LocalPlugin : IDisposable /// public bool IsDecommissioned => !this.IsDev && this.GetSourceRepository()?.State == PluginRepositoryState.Success && - this.GetSourceRepository()?.PluginMaster?.FirstOrDefault(x => x.InternalName == this.Manifest.InternalName) == null; + this.GetSourceRepository()?.PluginMaster?.FirstOrDefault(x => x.InternalName == this.manifest.InternalName) == null; /// /// Gets a value indicating whether this plugin has been banned. @@ -256,12 +257,23 @@ internal class LocalPlugin : IDisposable /// public bool IsDev => this is LocalDevPlugin; + /// + /// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party + /// repo. + /// + public bool IsThirdParty => this.manifest.IsThirdParty; + /// /// Gets a value indicating whether this plugin should be allowed to load. /// public bool ApplicableForLoad => !this.IsBanned && !this.IsDecommissioned && !this.IsOrphaned && !this.IsOutdated && !(!this.IsDev && this.State == PluginState.UnloadError) && this.CheckPolicy(); + /// + /// Gets the effective version of this plugin. + /// + public Version EffectiveVersion => this.manifest.EffectiveVersion; + /// /// Gets the service scope for this plugin. /// @@ -277,7 +289,7 @@ internal class LocalPlugin : IDisposable if (this.instance != null) { didPluginDispose = true; - if (this.Manifest.CanUnloadAsync || framework == null) + if (this.manifest.CanUnloadAsync || framework == null) this.instance.Dispose(); else framework.RunOnFrameworkThread(() => this.instance.Dispose()).Wait(); @@ -316,7 +328,7 @@ internal class LocalPlugin : IDisposable await Service.GetAsync(); await Service.GetAsync(); - if (this.Manifest.LoadRequiredState == 0) + if (this.manifest.LoadRequiredState == 0) _ = await Service.GetAsync(); await this.pluginLoadStateLock.WaitAsync(); @@ -329,9 +341,9 @@ internal class LocalPlugin : IDisposable } // If we reload a plugin we don't want to delete it. Makes sense, right? - if (this.Manifest.ScheduledForDeletion) + if (this.manifest.ScheduledForDeletion) { - this.Manifest.ScheduledForDeletion = false; + this.manifest.ScheduledForDeletion = false; this.SaveManifest("Scheduled for deletion, but loading"); } @@ -363,13 +375,13 @@ internal class LocalPlugin : IDisposable throw new ArgumentOutOfRangeException(this.State.ToString()); } - if (pluginManager.IsManifestBanned(this.Manifest) && !this.IsDev) + if (pluginManager.IsManifestBanned(this.manifest) && !this.IsDev) throw new BannedPluginException($"Unable to load {this.Name}, banned"); - if (this.Manifest.ApplicableVersion < startInfo.GameVersion) + if (this.manifest.ApplicableVersion < startInfo.GameVersion) throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version"); - if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels) + if (this.manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels) throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level"); // We might want to throw here? @@ -390,8 +402,8 @@ internal class LocalPlugin : IDisposable { Log.Error( "==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", - this.Manifest.Author!, - this.Manifest.InternalName); + this.manifest.Author!, + this.manifest.InternalName); Log.Error( "YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!"); Log.Error( @@ -459,7 +471,7 @@ internal class LocalPlugin : IDisposable this.ServiceScope = ioc.GetScope(); this.ServiceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it - if (this.Manifest.LoadSync && this.Manifest.LoadRequiredState is 0 or 1) + if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1) { this.instance = await framework.RunOnFrameworkThread( () => this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin; @@ -480,10 +492,10 @@ internal class LocalPlugin : IDisposable } // In-case the manifest name was a placeholder. Can occur when no manifest was included. - if (this.Manifest.Name.IsNullOrEmpty() && !this.IsDev) + if (this.manifest.Name.IsNullOrEmpty() && !this.IsDev) { - this.Manifest.Name = this.instance.Name; - this.Manifest.Save(this.manifestFile, "manifest name null or empty"); + this.manifest.Name = this.instance.Name; + this.manifest.Save(this.manifestFile, "manifest name null or empty"); } this.State = PluginState.Loaded; @@ -515,7 +527,6 @@ internal class LocalPlugin : IDisposable { var configuration = Service.Get(); var framework = Service.GetNullable(); - var ioc = await Service.GetAsync(); await this.pluginLoadStateLock.WaitAsync(); try @@ -544,7 +555,7 @@ internal class LocalPlugin : IDisposable this.State = PluginState.Unloading; Log.Information($"Unloading {this.DllFile.Name}"); - if (this.Manifest.CanUnloadAsync || framework == null) + if (this.manifest.CanUnloadAsync || framework == null) this.instance?.Dispose(); else await framework.RunOnFrameworkThread(() => this.instance?.Dispose()); @@ -612,7 +623,7 @@ internal class LocalPlugin : IDisposable if (startInfo.NoLoadPlugins) return false; - if (startInfo.NoLoadThirdPartyPlugins && this.Manifest.IsThirdParty) + if (startInfo.NoLoadThirdPartyPlugins && this.manifest.IsThirdParty) return false; if (manager.SafeMode) @@ -627,7 +638,7 @@ internal class LocalPlugin : IDisposable /// Schedule or cancel the deletion. public void ScheduleDeletion(bool status = true) { - this.Manifest.ScheduledForDeletion = status; + this.manifest.ScheduledForDeletion = status; this.SaveManifest("scheduling for deletion"); } @@ -636,12 +647,12 @@ internal class LocalPlugin : IDisposable /// public void ReloadManifest() { - var manifest = LocalPluginManifest.GetManifestFile(this.DllFile); - if (manifest.Exists) + var manifestPath = LocalPluginManifest.GetManifestFile(this.DllFile); + if (manifestPath.Exists) { // var isDisabled = this.IsDisabled; // saving the internal state because it could have been deleted - this.Manifest = LocalPluginManifest.Load(manifest) ?? throw new Exception("Could not reload manifest."); - // this.Manifest.Disabled = isDisabled; + this.manifest = LocalPluginManifest.Load(manifestPath) ?? throw new Exception("Could not reload manifest."); + // this.manifest.Disabled = isDisabled; this.SaveManifest("dev reload"); } @@ -659,10 +670,10 @@ internal class LocalPlugin : IDisposable var repos = Service.Get().Repos; return repos.FirstOrDefault(x => { - if (!x.IsThirdParty && !this.Manifest.IsThirdParty) + if (!x.IsThirdParty && !this.manifest.IsThirdParty) return true; - return x.PluginMasterUrl == this.Manifest.InstalledFromUrl; + return x.PluginMasterUrl == this.manifest.InstalledFromUrl; }); } @@ -675,5 +686,5 @@ internal class LocalPlugin : IDisposable config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); } - private void SaveManifest(string reason) => this.Manifest.Save(this.manifestFile, reason); + private void SaveManifest(string reason) => this.manifest.Save(this.manifestFile, reason); } diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs index bcb8cd9e7..d6f8f99bf 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs @@ -11,7 +11,7 @@ namespace Dalamud.Plugin.Internal.Types; /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as /// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk. /// -internal record LocalPluginManifest : PluginManifest +internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest { /// /// Flag indicating that a plugin was installed from the official repo. @@ -38,16 +38,10 @@ internal record LocalPluginManifest : PluginManifest /// public bool Testing { get; set; } - /// - /// Gets or sets a value indicating whether the plugin should be deleted during the next cleanup. - /// + /// public bool ScheduledForDeletion { get; set; } - /// - /// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was - /// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null - /// when installed from the main repo. - /// + /// public string InstalledFromUrl { get; set; } = string.Empty; /// diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index 71051e666..effff824a 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -9,41 +9,29 @@ namespace Dalamud.Plugin.Internal.Types; /// /// Information about a plugin, packaged in a json file with the DLL. /// -internal record PluginManifest +internal record PluginManifest : IPluginManifest { - /// - /// Gets the author/s of the plugin. - /// + /// [JsonProperty] public string? Author { get; init; } - /// - /// Gets or sets the public name of the plugin. - /// + /// [JsonProperty] public string Name { get; set; } = null!; - /// - /// Gets a punchline of the plugins functions. - /// + /// [JsonProperty] public string? Punchline { get; init; } - /// - /// Gets a description of the plugins functions. - /// + /// [JsonProperty] public string? Description { get; init; } - /// - /// Gets a changelog. - /// + /// [JsonProperty] public string? Changelog { get; init; } - /// - /// Gets a list of tags defined on the plugin. - /// + /// [JsonProperty] public List? Tags { get; init; } @@ -60,33 +48,23 @@ internal record PluginManifest [JsonProperty] public bool IsHide { get; init; } - /// - /// Gets the internal name of the plugin, which should match the assembly name of the plugin. - /// + /// [JsonProperty] - public string InternalName { get; init; } = null!; + public string InternalName { get; set; } = null!; - /// - /// Gets the current assembly version of the plugin. - /// + /// [JsonProperty] - public Version AssemblyVersion { get; init; } = null!; + public Version AssemblyVersion { get; set; } = null!; - /// - /// Gets the current testing assembly version of the plugin. - /// + /// [JsonProperty] public Version? TestingAssemblyVersion { get; init; } - /// - /// Gets a value indicating whether the plugin is only available for testing. - /// + /// [JsonProperty] public bool IsTestingExclusive { get; init; } - /// - /// Gets an URL to the website or source code of the plugin. - /// + /// [JsonProperty] public string? RepoUrl { get; init; } @@ -97,24 +75,17 @@ internal record PluginManifest [JsonConverter(typeof(GameVersionConverter))] public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any; - /// - /// Gets the API level of this plugin. For the current API level, please see - /// for the currently used API level. - /// + /// [JsonProperty] public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel; - /// - /// Gets the number of downloads this plugin has. - /// + /// [JsonProperty] public long DownloadCount { get; init; } - /// - /// Gets the last time this plugin was updated. - /// + /// [JsonProperty] - public long LastUpdate { get; init; } + public long LastUpdate { get; set; } /// /// Gets the download link used to install the plugin. @@ -156,26 +127,18 @@ internal record PluginManifest [JsonProperty] public int LoadPriority { get; init; } - /// - /// Gets a value indicating whether the plugin can be unloaded asynchronously. - /// + /// [JsonProperty] - public bool CanUnloadAsync { get; init; } + public bool CanUnloadAsync { get; set; } - /// - /// Gets a value indicating whether the plugin supports profiles. - /// + /// [JsonProperty] public bool SupportsProfiles { get; init; } = true; - /// - /// Gets a list of screenshot image URLs to show in the plugin installer. - /// + /// public List? ImageUrls { get; init; } - /// - /// Gets an URL for the plugin's icon. - /// + /// public string? IconUrl { get; init; } /// @@ -183,21 +146,10 @@ internal record PluginManifest /// public bool AcceptsFeedback { get; init; } = true; - /// - /// Gets a message that is shown to users when sending feedback. - /// + /// public string? FeedbackMessage { get; init; } - /// - /// Gets a value indicating whether this plugin is DIP17. - /// To be removed. - /// - [JsonProperty("_isDip17Plugin")] - public bool IsDip17Plugin { get; init; } = false; - - /// - /// Gets the DIP17 channel name. - /// + /// [JsonProperty("_Dip17Channel")] public string? Dip17Channel { get; init; } } diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index cdbf94616..da0df6054 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -25,7 +25,7 @@ internal static class BugBait /// The reporter name. /// Whether or not the most recent exception to occur should be included in the report. /// A representing the asynchronous operation. - public static async Task SendFeedback(PluginManifest plugin, bool isTesting, string content, string reporter, bool includeException) + public static async Task SendFeedback(IPluginManifest plugin, bool isTesting, string content, string reporter, bool includeException) { if (content.IsNullOrWhitespace()) return; diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index ef1897eeb..e8cf8d2eb 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -67,7 +67,7 @@ public static class Troubleshooting { var payload = new TroubleshootingPayload { - LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest)?.OrderByDescending(x => x.InternalName).ToArray(), + LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(), PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()), EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(), DalamudVersion = Util.AssemblyVersion, diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 60b1901d0..038273eb6 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -674,7 +674,7 @@ public static class Util } /// - /// Print formatted GameObject Information to ImGui + /// Print formatted GameObject Information to ImGui. /// /// Game Object to Display. /// Display Tag. From 875ca915b6be26547a647740d63de6629314b35e Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 29 Jun 2023 13:28:32 +0200 Subject: [PATCH 38/81] chore: codesigning for releases --- .github/workflows/main.yml | 5 +++ sign.ps1 | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 sign.ps1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d780df2fc..ccbedb74a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,6 +29,11 @@ jobs: run: .\build.ps1 compile - name: Test Dalamud run: .\build.ps1 test + - name: Sign Dalamud + env: + CODESIGN_CERT_PFX: ${{ secrets.CODESIGN_CERT_PFX }} + CODESIGN_CERT_PASSWORD: ${{ secrets.CODESIGN_CERT_PASSWORD }} + run: .\sign.ps1 .\bin\Release - name: Create hashlist run: .\CreateHashList.ps1 .\bin\Release - name: Upload artifact diff --git a/sign.ps1 b/sign.ps1 new file mode 100644 index 000000000..73fb6cc67 --- /dev/null +++ b/sign.ps1 @@ -0,0 +1,71 @@ +# Get the certificate and password from environment variables +$certificateBase64 = $env:CODESIGN_CERT_PFX +$certificatePassword = $env:CODESIGN_CERT_PASSWORD + +# Write the certificate to a file +$certificatePath = Join-Path -Path $env:TEMP -ChildPath 'certificate.pfx' +$certificateBytes = [Convert]::FromBase64String($certificateBase64) +[System.IO.File]::WriteAllBytes($certificatePath, $certificateBytes) + +# Define the function to find the path to signtool.exe +function Get-SignToolPath { + # Array of common installation directories for Windows SDK + $sdkInstallationDirs = @( + "$env:ProgramFiles (x86)\Windows Kits\10\bin\x64", + "$env:ProgramFiles\Windows Kits\10\bin\x64", + "$env:ProgramFiles (x86)\Windows Kits\10\App Certification Kit" + ) + + foreach ($dir in $sdkInstallationDirs) { + $path = Join-Path -Path $dir -ChildPath 'signtool.exe' + #Write-Host $path + if (Test-Path -Path $path) { + return $path + } + } + + throw "Could not find signtool.exe. Make sure the Windows SDK is installed." +} + +# Find the path to signtool.exe +$signtoolPath = Get-SignToolPath + +# Define the function to code-sign a file +function Sign-File { + param ( + [Parameter(Mandatory=$true)] + [String]$FilePath + ) + + # Check if the file is already code-signed + $signature = Get-AuthenticodeSignature -FilePath $FilePath -ErrorAction SilentlyContinue + if ($signature.status -ne "NotSigned") { + Write-Host "File '$FilePath' is already code-signed. Skipping." + return + } + + # Code-sign the file using signtool + Write-Host "Code-signing file '$FilePath'..." + & $signtoolPath sign /tr http://timestamp.digicert.com /td sha256 /v /fd sha256 /f $certificatePath /p $certificatePassword $FilePath +} + +# Define the function to recursively code-sign files in a directory +function Sign-FilesRecursively { + param ( + [Parameter(Mandatory=$true)] + [String]$DirectoryPath + ) + + Write-Host $DirectoryPath + + # Get all exe and dll files recursively + dir $DirectoryPath -recurse | where {$_.extension -in ".exe",".dll"} | ForEach-Object { + Sign-File -FilePath $_.FullName + } +} + +# Usage: Provide the directory path as an argument to sign files recursively +Sign-FilesRecursively -DirectoryPath $args[0] + +# Remove the temporary certificate file +Remove-Item -Path $certificatePath From 7ada7eb4e7d27f9793b8dcf5dec0d48ce120e523 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 29 Jun 2023 12:51:11 -0700 Subject: [PATCH 39/81] chore: Only sign on main repo (#1297) Disables signing on external workflows/branches. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ccbedb74a..e8c521195 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,7 @@ jobs: - name: Test Dalamud run: .\build.ps1 test - name: Sign Dalamud + if: ${{ github.repository_owner == 'goatcorp' }} env: CODESIGN_CERT_PFX: ${{ secrets.CODESIGN_CERT_PFX }} CODESIGN_CERT_PASSWORD: ${{ secrets.CODESIGN_CERT_PASSWORD }} From ed21ba8b08b40ada8ecb2b1e208192783f92c494 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jul 2023 11:53:23 +0200 Subject: [PATCH 40/81] Improve network monitor somewhat. Add negative filtering. --- .../Data/Widgets/NetworkMonitorWidget.cs | 68 ++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 488f46fed..ce1559fc8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Text.RegularExpressions; using Dalamud.Data; @@ -27,12 +28,14 @@ internal class NetworkMonitorWidget : IDataWindowWidget } private readonly ConcurrentQueue packets = new(); - private readonly Dictionary opCodeDict = new(); + private readonly Dictionary opCodeDict = new(); private bool trackNetwork; private int trackedPackets; private Regex? trackedOpCodes; private string filterString = string.Empty; + private Regex? untrackedOpCodes; + private string negativeFilterString = string.Empty; /// Finalizes an instance of the class. ~NetworkMonitorWidget() @@ -91,8 +94,9 @@ internal class NetworkMonitorWidget : IDataWindowWidget } this.DrawFilterInput(); + this.DrawNegativeFilterInput(); - ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "OpCode", "Source", "Target", "Data"); + ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "Known Name", "OpCode", "Hex", "Target", "Source", "Data"); } private void DrawNetworkPacket(NetworkPacketData data) @@ -101,19 +105,36 @@ internal class NetworkMonitorWidget : IDataWindowWidget ImGui.TextUnformatted(data.Direction.ToString()); ImGui.TableNextColumn(); - ImGui.TextUnformatted(this.opCodeDict.TryGetValue(data.OpCode, out var pair) ? pair.Item1 : data.OpCode.ToString()); + if (this.opCodeDict.TryGetValue(data.OpCode, out var pair)) + { + ImGui.TextUnformatted(pair.Name); + } + else + { + ImGui.Dummy(new Vector2(150 * ImGuiHelpers.GlobalScale, 0)); + } ImGui.TableNextColumn(); - ImGui.TextUnformatted($"0x{data.SourceActorId:X}"); + ImGui.TextUnformatted(data.OpCode.ToString()); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"0x{data.TargetActorId:X}"); + ImGui.TextUnformatted($"0x{data.OpCode:X4}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(data.TargetActorId > 0 ? $"0x{data.TargetActorId:X}" : string.Empty); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(data.SourceActorId > 0 ? $"0x{data.SourceActorId:X}" : string.Empty); ImGui.TableNextColumn(); if (data.Data.Count > 0) { ImGui.TextUnformatted(string.Join(" ", data.Data.Select(b => b.ToString("X2")))); } + else + { + ImGui.Dummy(ImGui.GetContentRegionAvail() with { Y = 0 }); + } } private void DrawFilterInput() @@ -122,7 +143,7 @@ internal class NetworkMonitorWidget : IDataWindowWidget using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx); using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (!ImGui.InputTextWithHint("##Filter", "Regex Filter OpCodes...", ref this.filterString, 256)) + if (!ImGui.InputTextWithHint("##Filter", "Regex Filter OpCodes...", ref this.filterString, 1024)) { return; } @@ -144,9 +165,38 @@ internal class NetworkMonitorWidget : IDataWindowWidget } } + private void DrawNegativeFilterInput() + { + var invalidRegEx = this.negativeFilterString.Length > 0 && this.untrackedOpCodes == null; + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx); + using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (!ImGui.InputTextWithHint("##NegativeFilter", "Regex Filter Against OpCodes...", ref this.negativeFilterString, 1024)) + { + return; + } + + if (this.negativeFilterString.Length == 0) + { + this.untrackedOpCodes = null; + } + else + { + try + { + this.untrackedOpCodes = new Regex(this.negativeFilterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture); + } + catch + { + this.untrackedOpCodes = null; + } + } + } + private void OnNetworkMessage(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) { - if (this.trackedOpCodes == null || this.trackedOpCodes.IsMatch(this.OpCodeToString(opCode))) + if ((this.trackedOpCodes == null || this.trackedOpCodes.IsMatch(this.OpCodeToString(opCode))) + && (this.untrackedOpCodes == null || !this.untrackedOpCodes.IsMatch(this.OpCodeToString(opCode)))) { this.packets.Enqueue(new NetworkPacketData(this, opCode, direction, sourceActorId, targetActorId, dataPtr)); while (this.packets.Count > this.trackedPackets) @@ -157,7 +207,7 @@ internal class NetworkMonitorWidget : IDataWindowWidget } private int GetSizeFromOpCode(ushort opCode) - => this.opCodeDict.TryGetValue(opCode, out var pair) ? pair.Item2 : 0; + => this.opCodeDict.TryGetValue(opCode, out var pair) ? pair.Size : 0; /// Add known packet-name -> packet struct size associations here to copy the byte data for such packets. > private int GetSizeFromName(string name) @@ -168,5 +218,5 @@ internal class NetworkMonitorWidget : IDataWindowWidget /// The filter should find opCodes by number (decimal and hex) and name, if existing. private string OpCodeToString(ushort opCode) - => this.opCodeDict.TryGetValue(opCode, out var pair) ? $"{pair.Item1}\0{opCode}\0{opCode:X}" : $"{opCode}\0{opCode:X}"; + => this.opCodeDict.TryGetValue(opCode, out var pair) ? $"{opCode}\0{opCode:X}\0{pair.Name}" : $"{opCode}\0{opCode:X}"; } From 9a429ef9f4055c15bf8030f37d68290f9984f42d Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 2 Jul 2023 18:00:24 +0200 Subject: [PATCH 41/81] chore: some more manifest refactoring --- .../Interface/Internal/Windows/PluginImageCache.cs | 1 + .../PluginInstaller/PluginInstallerWindow.cs | 13 ++++++++----- Dalamud/Plugin/DalamudPluginInterface.cs | 1 + Dalamud/Plugin/Internal/PluginManager.cs | 1 + .../Plugin/Internal/Types/AvailablePluginUpdate.cs | 2 ++ Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs | 1 + Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 1 + .../Types/{ => Manifest}/ILocalPluginManifest.cs | 2 +- .../Types/{ => Manifest}/IPluginManifest.cs | 2 +- .../Types/{ => Manifest}/LocalPluginManifest.cs | 7 +------ .../Types/{ => Manifest}/RemotePluginManifest.cs | 7 ++++++- Dalamud/Plugin/Internal/Types/PluginDef.cs | 2 ++ Dalamud/Plugin/Internal/Types/PluginManifest.cs | 1 + Dalamud/Plugin/Internal/Types/PluginRepository.cs | 1 + Dalamud/Support/BugBait.cs | 2 +- Dalamud/Support/Troubleshooting.cs | 2 +- 16 files changed, 30 insertions(+), 16 deletions(-) rename Dalamud/Plugin/Internal/Types/{ => Manifest}/ILocalPluginManifest.cs (92%) rename Dalamud/Plugin/Internal/Types/{ => Manifest}/IPluginManifest.cs (98%) rename Dalamud/Plugin/Internal/Types/{ => Manifest}/LocalPluginManifest.cs (94%) rename Dalamud/Plugin/Internal/Types/{ => Manifest}/RemotePluginManifest.cs (71%) diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index bcdf90fe4..766f80b23 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -11,6 +11,7 @@ using Dalamud.Game; using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Utility; using ImGuiScene; using Serilog; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 80b4656b0..a37f05b68 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -24,6 +24,7 @@ using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Support; using Dalamud.Utility; using ImGuiNET; @@ -2036,13 +2037,12 @@ internal class PluginInstallerWindow : Window, IDisposable label += Locs.PluginTitleMod_TestingVersion; } - // TODO: check with the repos instead - /* - if (plugin.Manifest.IsAvailableForTesting && configuration.DoPluginTest && testingOptIn == null) + var hasTestingAvailable = this.pluginListAvailable.Any(x => x.InternalName == plugin.InternalName && + x.IsAvailableForTesting); + if (hasTestingAvailable && configuration.DoPluginTest && testingOptIn == null) { label += Locs.PluginTitleMod_TestingAvailable; } - */ // Freshly installed if (showInstalled) @@ -2157,12 +2157,15 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SameLine(); ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadText); + var acceptsFeedback = + this.pluginListAvailable.Any(x => x.InternalName == plugin.InternalName && x.AcceptsFeedback); + var isThirdParty = plugin.IsThirdParty; var canFeedback = !isThirdParty && !plugin.IsDev && !plugin.IsOrphaned && plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel && - // plugin.Manifest.AcceptsFeedback && // TODO: check with the repos + acceptsFeedback && availablePluginUpdate == default; // Installed from diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 1b9f065ab..2b58c21cc 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -21,6 +21,7 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index a7fec04e1..c98a6bbfc 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -25,6 +25,7 @@ using Dalamud.Networking.Http; using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc.Internal; using Dalamud.Utility; using Dalamud.Utility.Timing; diff --git a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs index 13523a379..36823b389 100644 --- a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs +++ b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Internal.Types.Manifest; + namespace Dalamud.Plugin.Internal.Types; /// diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 498bea874..98784ce64 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types.Manifest; namespace Dalamud.Plugin.Internal.Types; diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index d4caed0fd..5ad09ebc3 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -15,6 +15,7 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Loader; using Dalamud.Plugin.Internal.Profiles; +using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Utility; namespace Dalamud.Plugin.Internal.Types; diff --git a/Dalamud/Plugin/Internal/Types/ILocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs similarity index 92% rename from Dalamud/Plugin/Internal/Types/ILocalPluginManifest.cs rename to Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs index bbd40d2dd..97365d1e5 100644 --- a/Dalamud/Plugin/Internal/Types/ILocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Plugin.Internal.Types; +namespace Dalamud.Plugin.Internal.Types.Manifest; /// /// Public interface for the local plugin manifest. diff --git a/Dalamud/Plugin/Internal/Types/IPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs similarity index 98% rename from Dalamud/Plugin/Internal/Types/IPluginManifest.cs rename to Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs index 90f2fb8bd..9e052efad 100644 --- a/Dalamud/Plugin/Internal/Types/IPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Dalamud.Plugin.Internal.Types; +namespace Dalamud.Plugin.Internal.Types.Manifest; /// /// Public interface for the base plugin manifest. diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs similarity index 94% rename from Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs rename to Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs index d6f8f99bf..4b4bf5d6e 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs @@ -5,7 +5,7 @@ using Dalamud.Utility; using Newtonsoft.Json; using Serilog; -namespace Dalamud.Plugin.Internal.Types; +namespace Dalamud.Plugin.Internal.Types.Manifest; /// /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as @@ -55,11 +55,6 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest /// public Version EffectiveVersion => this.Testing && this.TestingAssemblyVersion != null ? this.TestingAssemblyVersion : this.AssemblyVersion; - /// - /// Gets a value indicating whether this plugin is eligible for testing. - /// - public bool IsAvailableForTesting => this.TestingAssemblyVersion != null && this.TestingAssemblyVersion > this.AssemblyVersion; - /// /// Save a plugin manifest to file. /// diff --git a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs similarity index 71% rename from Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs rename to Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs index 09084d569..952650c72 100644 --- a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs @@ -1,7 +1,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; -namespace Dalamud.Plugin.Internal.Types; +namespace Dalamud.Plugin.Internal.Types.Manifest; /// /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as @@ -16,4 +16,9 @@ internal record RemotePluginManifest : PluginManifest /// [JsonIgnore] public PluginRepository SourceRepo { get; set; } = null!; + + /// + /// Gets a value indicating whether this plugin is eligible for testing. + /// + public bool IsAvailableForTesting => this.TestingAssemblyVersion != null && this.TestingAssemblyVersion > this.AssemblyVersion; } diff --git a/Dalamud/Plugin/Internal/Types/PluginDef.cs b/Dalamud/Plugin/Internal/Types/PluginDef.cs index c5dbede0d..049e58d7d 100644 --- a/Dalamud/Plugin/Internal/Types/PluginDef.cs +++ b/Dalamud/Plugin/Internal/Types/PluginDef.cs @@ -1,5 +1,7 @@ using System.IO; +using Dalamud.Plugin.Internal.Types.Manifest; + namespace Dalamud.Plugin.Internal.Types; /// diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index effff824a..0b5ec26fc 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Dalamud.Game; +using Dalamud.Plugin.Internal.Types.Manifest; using Newtonsoft.Json; namespace Dalamud.Plugin.Internal.Types; diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index b51a3355c..a1097abce 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Networking.Http; +using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Utility; using Newtonsoft.Json; diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index da0df6054..22628303e 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -3,7 +3,7 @@ using System.Text; using System.Threading.Tasks; using Dalamud.Networking.Http; -using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Utility; using Newtonsoft.Json; diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index e8cf8d2eb..9893451f4 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -7,7 +7,7 @@ using Dalamud.Configuration; using Dalamud.Configuration.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; -using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Utility; using Newtonsoft.Json; using Serilog; From e4a7224f4194a7afe56611a51a21b26fbd384b5b Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 2 Jul 2023 18:03:19 +0200 Subject: [PATCH 42/81] chore: rename "delete plugin config" options to "delete plugin data" --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index a37f05b68..05376a770 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3130,9 +3130,9 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer"); - public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin configuration"); + public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data"); - public static string PluginContext_DeletePluginConfigReload => Loc.Localize("InstallerDeletePluginConfigReload", "Reset plugin configuration and reload"); + public static string PluginContext_DeletePluginConfigReload => Loc.Localize("InstallerDeletePluginConfigReload", "Reset plugin data and reload"); #endregion From 40aa70d05f7f46fee125b1a6cbdd0f0d0d88beb7 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 2 Jul 2023 09:12:41 -0700 Subject: [PATCH 43/81] Add IDataManager (#1292) --- Dalamud/Data/DataManager.cs | 162 +++++---------------- Dalamud/Plugin/Services/IDataManager.cs | 180 ++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 127 deletions(-) create mode 100644 Dalamud/Plugin/Services/IDataManager.cs diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 9d6a352ca..407a1b0da 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -8,6 +8,7 @@ using System.Threading; using Dalamud.Interface.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiScene; @@ -27,7 +28,10 @@ namespace Dalamud.Data; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class DataManager : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed class DataManager : IDisposable, IServiceType, IDataManager { private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; @@ -127,75 +131,43 @@ public sealed class DataManager : IDisposable, IServiceType } } - /// - /// Gets the current game client language. - /// + /// public ClientLanguage Language { get; private set; } - /// - /// Gets the OpCodes sent by the server to the client. - /// + /// public ReadOnlyDictionary ServerOpCodes { get; private set; } - /// - /// Gets the OpCodes sent by the client to the server. - /// + /// [UsedImplicitly] public ReadOnlyDictionary ClientOpCodes { get; private set; } - /// - /// Gets a object which gives access to any excel/game data. - /// + /// public GameData GameData { get; private set; } - /// - /// Gets an object which gives access to any of the game's sheet data. - /// + /// public ExcelModule Excel => this.GameData.Excel; - /// - /// Gets a value indicating whether Game Data is ready to be read. - /// + /// public bool IsDataReady { get; private set; } - /// - /// Gets a value indicating whether the game data files have been modified by another third-party tool. - /// + /// public bool HasModifiedGameDataFiles { get; private set; } #region Lumina Wrappers - /// - /// Get an with the given Excel sheet row type. - /// - /// The excel sheet type to get. - /// The , giving access to game rows. + /// public ExcelSheet? GetExcelSheet() where T : ExcelRow => this.Excel.GetSheet(); - /// - /// Get an with the given Excel sheet row type with a specified language. - /// - /// Language of the sheet to get. - /// The excel sheet type to get. - /// The , giving access to game rows. + /// public ExcelSheet? GetExcelSheet(ClientLanguage language) where T : ExcelRow => this.Excel.GetSheet(language.ToLumina()); - /// - /// Get a with the given path. - /// - /// The path inside of the game files. - /// The of the file. + /// public FileResource? GetFile(string path) => this.GetFile(path); - /// - /// Get a with the given path, of the given type. - /// - /// The type of resource. - /// The path inside of the game files. - /// The of the file. + /// public T? GetFile(string path) where T : FileResource { var filePath = GameData.ParseFilePath(path); @@ -204,11 +176,7 @@ public sealed class DataManager : IDisposable, IServiceType return this.GameData.Repositories.TryGetValue(filePath.Repository, out var repository) ? repository.GetFile(filePath.Category, filePath) : default; } - /// - /// Check if the file with the given path exists within the game's index files. - /// - /// The path inside of the game files. - /// True if the file exists. + /// public bool FileExists(string path) => this.GameData.FileExists(path); @@ -217,25 +185,15 @@ public sealed class DataManager : IDisposable, IServiceType /// /// The icon ID. /// The containing the icon. - /// todo: remove in api9 in favor of GetIcon(uint iconId, bool highResolution) + /// TODO(v9): remove in api9 in favor of GetIcon(uint iconId, bool highResolution) public TexFile? GetIcon(uint iconId) => this.GetIcon(this.Language, iconId, false); - /// - /// Get a containing the icon with the given ID. - /// - /// The icon ID. - /// Return high resolution version. - /// The containing the icon. + /// public TexFile? GetIcon(uint iconId, bool highResolution) => this.GetIcon(this.Language, iconId, highResolution); - - /// - /// Get a containing the icon with the given ID, of the given quality. - /// - /// A value indicating whether the icon should be HQ. - /// The icon ID. - /// The containing the icon. + + /// public TexFile? GetIcon(bool isHq, uint iconId) { var type = isHq ? "hq/" : string.Empty; @@ -248,17 +206,11 @@ public sealed class DataManager : IDisposable, IServiceType /// The requested language. /// The icon ID. /// The containing the icon. - /// todo: remove in api9 in favor of GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution) + /// TODO(v9): remove in api9 in favor of GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution) public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId) => this.GetIcon(iconLanguage, iconId, false); - /// - /// Get a containing the icon with the given ID, of the given language. - /// - /// The requested language. - /// The icon ID. - /// Return high resolution version. - /// The containing the icon. + /// public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution) { var type = iconLanguage switch @@ -279,17 +231,11 @@ public sealed class DataManager : IDisposable, IServiceType /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). /// The icon ID. /// The containing the icon. - /// todo: remove in api9 in favor of GetIcon(string? type, uint iconId, bool highResolution) + /// TODO(v9): remove in api9 in favor of GetIcon(string? type, uint iconId, bool highResolution) public TexFile? GetIcon(string? type, uint iconId) => this.GetIcon(type, iconId, false); - /// - /// Get a containing the icon with the given ID, of the given type. - /// - /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). - /// The icon ID. - /// Return high resolution version. - /// The containing the icon. + /// public TexFile? GetIcon(string? type, uint iconId, bool highResolution) { var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; @@ -310,27 +256,15 @@ public sealed class DataManager : IDisposable, IServiceType return file; } - /// - /// Get a containing the HQ icon with the given ID. - /// - /// The icon ID. - /// The containing the icon. + /// public TexFile? GetHqIcon(uint iconId) => this.GetIcon(true, iconId); - /// - /// Get the passed as a drawable ImGui TextureWrap. - /// - /// The Lumina . - /// A that can be used to draw the texture. + /// public TextureWrap? GetImGuiTexture(TexFile? tex) => tex == null ? null : Service.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4); - /// - /// Get the passed texture path as a drawable ImGui TextureWrap. - /// - /// The internal path to the texture. - /// A that can be used to draw the texture. + /// public TextureWrap? GetImGuiTexture(string path) => this.GetImGuiTexture(this.GetFile(path)); @@ -339,59 +273,33 @@ public sealed class DataManager : IDisposable, IServiceType /// /// The icon ID. /// The containing the icon. - /// todo: remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution) + /// TODO(v9): remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution) public TextureWrap? GetImGuiTextureIcon(uint iconId) => this.GetImGuiTexture(this.GetIcon(iconId, false)); - /// - /// Get a containing the icon with the given ID. - /// - /// The icon ID. - /// Return the high resolution version. - /// The containing the icon. + /// public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution) => this.GetImGuiTexture(this.GetIcon(iconId, highResolution)); - /// - /// Get a containing the icon with the given ID, of the given quality. - /// - /// A value indicating whether the icon should be HQ. - /// The icon ID. - /// The containing the icon. + /// public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId) => this.GetImGuiTexture(this.GetIcon(isHq, iconId)); - /// - /// Get a containing the icon with the given ID, of the given language. - /// - /// The requested language. - /// The icon ID. - /// The containing the icon. + /// public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId) => this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId)); - /// - /// Get a containing the icon with the given ID, of the given type. - /// - /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). - /// The icon ID. - /// The containing the icon. + /// public TextureWrap? GetImGuiTextureIcon(string type, uint iconId) => this.GetImGuiTexture(this.GetIcon(type, iconId)); - /// - /// Get a containing the HQ icon with the given ID. - /// - /// The icon ID. - /// The containing the icon. + /// public TextureWrap? GetImGuiTextureHqIcon(uint iconId) => this.GetImGuiTexture(this.GetHqIcon(iconId)); #endregion - /// - /// Dispose this DataManager. - /// + /// void IDisposable.Dispose() { this.luminaCancellationTokenSource.Cancel(); diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs new file mode 100644 index 000000000..69a3e9a21 --- /dev/null +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -0,0 +1,180 @@ +using System.Collections.ObjectModel; +using ImGuiScene; +using Lumina; +using Lumina.Data; +using Lumina.Data.Files; +using Lumina.Excel; + +namespace Dalamud.Plugin.Services; + +/// +/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed. +/// +public interface IDataManager +{ + /// + /// Gets the current game client language. + /// + public ClientLanguage Language { get; } + + /// + /// Gets the OpCodes sent by the server to the client. + /// + public ReadOnlyDictionary ServerOpCodes { get; } + + /// + /// Gets the OpCodes sent by the client to the server. + /// + public ReadOnlyDictionary ClientOpCodes { get; } + + /// + /// Gets a object which gives access to any excel/game data. + /// + public GameData GameData { get; } + + /// + /// Gets an object which gives access to any of the game's sheet data. + /// + public ExcelModule Excel { get; } + + /// + /// Gets a value indicating whether Game Data is ready to be read. + /// + public bool IsDataReady { get; } + + /// + /// Gets a value indicating whether the game data files have been modified by another third-party tool. + /// + public bool HasModifiedGameDataFiles { get; } + + /// + /// Get an with the given Excel sheet row type. + /// + /// The excel sheet type to get. + /// The , giving access to game rows. + public ExcelSheet? GetExcelSheet() where T : ExcelRow; + + /// + /// Get an with the given Excel sheet row type with a specified language. + /// + /// Language of the sheet to get. + /// The excel sheet type to get. + /// The , giving access to game rows. + public ExcelSheet? GetExcelSheet(ClientLanguage language) where T : ExcelRow; + + /// + /// Get a with the given path. + /// + /// The path inside of the game files. + /// The of the file. + public FileResource? GetFile(string path); + + /// + /// Get a with the given path, of the given type. + /// + /// The type of resource. + /// The path inside of the game files. + /// The of the file. + public T? GetFile(string path) where T : FileResource; + + /// + /// Check if the file with the given path exists within the game's index files. + /// + /// The path inside of the game files. + /// True if the file exists. + public bool FileExists(string path); + + /// + /// Get a containing the icon with the given ID. + /// + /// The icon ID. + /// Return high resolution version. + /// The containing the icon. + public TexFile? GetIcon(uint iconId, bool highResolution = false); + + /// + /// Get a containing the icon with the given ID, of the given language. + /// + /// The requested language. + /// The icon ID. + /// Return high resolution version. + /// The containing the icon. + public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false); + + /// + /// Get a containing the icon with the given ID, of the given type. + /// + /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). + /// The icon ID. + /// Return high resolution version. + /// The containing the icon. + public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false); + + /// + /// Get a containing the icon with the given ID. + /// + /// The icon ID. + /// Return the high resolution version. + /// The containing the icon. + public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false); + + /// + /// Get a containing the icon with the given ID, of the given quality. + /// + /// A value indicating whether the icon should be HQ. + /// The icon ID. + /// The containing the icon. + public TexFile? GetIcon(bool isHq, uint iconId); + + /// + /// Get a containing the HQ icon with the given ID. + /// + /// The icon ID. + /// The containing the icon. + public TexFile? GetHqIcon(uint iconId); + + /// + /// Get the passed as a drawable ImGui TextureWrap. + /// + /// The Lumina . + /// A that can be used to draw the texture. + public TextureWrap? GetImGuiTexture(TexFile? tex); + + /// + /// Get the passed texture path as a drawable ImGui TextureWrap. + /// + /// The internal path to the texture. + /// A that can be used to draw the texture. + public TextureWrap? GetImGuiTexture(string path); + + /// + /// Get a containing the icon with the given ID, of the given quality. + /// + /// A value indicating whether the icon should be HQ. + /// The icon ID. + /// The containing the icon. + public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId); + + /// + /// Get a containing the icon with the given ID, of the given language. + /// + /// The requested language. + /// The icon ID. + /// The containing the icon. + public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId); + + /// + /// Get a containing the icon with the given ID, of the given type. + /// + /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). + /// The icon ID. + /// The containing the icon. + public TextureWrap? GetImGuiTextureIcon(string type, uint iconId); + + /// + /// Get a containing the HQ icon with the given ID. + /// + /// The icon ID. + /// The containing the icon. + public TextureWrap? GetImGuiTextureHqIcon(uint iconId); +} From 3bf1a05f7d1e4d365f2dffa2e4c095e5e8ac4a03 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 2 Jul 2023 09:13:02 -0700 Subject: [PATCH 44/81] GamepadWidget resolve obsoletes (#1291) --- .../Windows/Data/Widgets/GamepadWidget.cs | 60 +++++++++---------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs index 5c92e3ad1..1a4408d53 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs @@ -27,27 +27,6 @@ internal class GamepadWidget : IDataWindowWidget { var gamepadState = Service.Get(); - static void DrawHelper(string text, uint mask, Func resolve) - { - ImGui.Text($"{text} {mask:X4}"); - ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " + - $"DPadUp {resolve(GamepadButtons.DpadUp)} " + - $"DPadRight {resolve(GamepadButtons.DpadRight)} " + - $"DPadDown {resolve(GamepadButtons.DpadDown)} "); - ImGui.Text($"West {resolve(GamepadButtons.West)} " + - $"North {resolve(GamepadButtons.North)} " + - $"East {resolve(GamepadButtons.East)} " + - $"South {resolve(GamepadButtons.South)} "); - ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " + - $"L2 {resolve(GamepadButtons.L2)} " + - $"R1 {resolve(GamepadButtons.R1)} " + - $"R2 {resolve(GamepadButtons.R2)} "); - ImGui.Text($"Select {resolve(GamepadButtons.Select)} " + - $"Start {resolve(GamepadButtons.Start)} " + - $"L3 {resolve(GamepadButtons.L3)} " + - $"R3 {resolve(GamepadButtons.R3)} "); - } - ImGui.Text($"GamepadInput 0x{gamepadState.GamepadInputAddress.ToInt64():X}"); #if DEBUG @@ -58,29 +37,44 @@ internal class GamepadWidget : IDataWindowWidget ImGui.SetClipboardText($"0x{gamepadState.GamepadInputAddress.ToInt64():X}"); #endif - DrawHelper( + this.DrawHelper( "Buttons Raw", gamepadState.ButtonsRaw, gamepadState.Raw); - DrawHelper( + this.DrawHelper( "Buttons Pressed", gamepadState.ButtonsPressed, gamepadState.Pressed); - DrawHelper( + this.DrawHelper( "Buttons Repeat", gamepadState.ButtonsRepeat, gamepadState.Repeat); - DrawHelper( + this.DrawHelper( "Buttons Released", gamepadState.ButtonsReleased, gamepadState.Released); - ImGui.Text($"LeftStickLeft {gamepadState.LeftStickLeft:0.00} " + - $"LeftStickUp {gamepadState.LeftStickUp:0.00} " + - $"LeftStickRight {gamepadState.LeftStickRight:0.00} " + - $"LeftStickDown {gamepadState.LeftStickDown:0.00} "); - ImGui.Text($"RightStickLeft {gamepadState.RightStickLeft:0.00} " + - $"RightStickUp {gamepadState.RightStickUp:0.00} " + - $"RightStickRight {gamepadState.RightStickRight:0.00} " + - $"RightStickDown {gamepadState.RightStickDown:0.00} "); + ImGui.Text($"LeftStick {gamepadState.LeftStick}"); + ImGui.Text($"RightStick {gamepadState.RightStick}"); + } + + private void DrawHelper(string text, uint mask, Func resolve) + { + ImGui.Text($"{text} {mask:X4}"); + ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " + + $"DPadUp {resolve(GamepadButtons.DpadUp)} " + + $"DPadRight {resolve(GamepadButtons.DpadRight)} " + + $"DPadDown {resolve(GamepadButtons.DpadDown)} "); + ImGui.Text($"West {resolve(GamepadButtons.West)} " + + $"North {resolve(GamepadButtons.North)} " + + $"East {resolve(GamepadButtons.East)} " + + $"South {resolve(GamepadButtons.South)} "); + ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " + + $"L2 {resolve(GamepadButtons.L2)} " + + $"R1 {resolve(GamepadButtons.R1)} " + + $"R2 {resolve(GamepadButtons.R2)} "); + ImGui.Text($"Select {resolve(GamepadButtons.Select)} " + + $"Start {resolve(GamepadButtons.Start)} " + + $"L3 {resolve(GamepadButtons.L3)} " + + $"R3 {resolve(GamepadButtons.R3)} "); } } From 1cc07cc3d149a51a334b99a2b11019b9b9f0bf50 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 2 Jul 2023 09:13:21 -0700 Subject: [PATCH 45/81] TargetWidget resolve obsoletes (#1290) --- .../Internal/Windows/Data/Widgets/TargetWidget.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index 07d6e8f72..57fd03300 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -65,20 +65,20 @@ internal class TargetWidget : IDataWindowWidget Util.PrintGameObject(targetMgr.SoftTarget, "SoftTarget", this.resolveGameData); if (ImGui.Button("Clear CT")) - targetMgr.ClearTarget(); + targetMgr.Target = null; if (ImGui.Button("Clear FT")) - targetMgr.ClearFocusTarget(); + targetMgr.FocusTarget = null; var localPlayer = clientState.LocalPlayer; if (localPlayer != null) { if (ImGui.Button("Set CT")) - targetMgr.SetTarget(localPlayer); + targetMgr.Target = localPlayer; if (ImGui.Button("Set FT")) - targetMgr.SetFocusTarget(localPlayer); + targetMgr.FocusTarget = localPlayer; } else { From f532f6e2597252771da3937e355a086192865edd Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 2 Jul 2023 18:15:40 +0200 Subject: [PATCH 46/81] [master] Update ClientStructs (#1260) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index f2abb4a11..20cdc0e0f 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit f2abb4a11319b26b77cd29b69a52b34e1d56069d +Subproject commit 20cdc0e0fab656bb98b34f593fefdfe15b9df380 From c93d7f16ed20082a23ef778f9cd4d116e5c371df Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 2 Jul 2023 18:30:15 +0200 Subject: [PATCH 47/81] chore: fix warnings --- Dalamud/Game/ClientState/Fates/FateTable.cs | 2 +- .../Game/ClientState/Objects/TargetManager.cs | 1 + Dalamud/Interface/Internal/InterfaceManager.cs | 8 ++++---- .../Windows/Data/Widgets/NetworkMonitorWidget.cs | 2 ++ .../PluginInstaller/ProfileManagerWidget.cs | 16 +++++++++------- .../SelfTest/AgingSteps/TargetAgingStep.cs | 4 ++-- Dalamud/Plugin/Services/IDataManager.cs | 1 + 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 8416f0ffb..53196d5df 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -17,7 +17,7 @@ namespace Dalamud.Game.ClientState.Fates; [ServiceManager.BlockingEarlyLoadedService] #pragma warning disable SA1015 [ResolveVia] -#pragma warning enable SA1015 +#pragma warning restore SA1015 public sealed partial class FateTable : IServiceType, IFateTable { private readonly ClientStateAddressResolver address; diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index c4c1e3822..ff1bdc5ba 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -3,6 +3,7 @@ using System; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.IoC; using Dalamud.IoC.Internal; +#pragma warning disable CS0618 namespace Dalamud.Game.ClientState.Objects; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 5de5f52de..6bb45b325 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1147,10 +1147,10 @@ internal class InterfaceManager : IDisposable, IServiceType var dPadRight = gamepadState.Raw(GamepadButtons.DpadRight) != 0; var dPadDown = gamepadState.Raw(GamepadButtons.DpadDown) != 0; var dPadLeft = gamepadState.Raw(GamepadButtons.DpadLeft) != 0; - var leftStickUp = gamepadState.LeftStickUp; - var leftStickRight = gamepadState.LeftStickRight; - var leftStickDown = gamepadState.LeftStickDown; - var leftStickLeft = gamepadState.LeftStickLeft; + var leftStickUp = gamepadState.LeftStick.Y > 0 ? gamepadState.LeftStick.Y / 100f : 0; + var leftStickRight = gamepadState.LeftStick.X > 0 ? gamepadState.LeftStick.X / 100f : 0; + var leftStickDown = gamepadState.LeftStick.Y < 0 ? -gamepadState.LeftStick.Y / 100f : 0; + var leftStickLeft = gamepadState.LeftStick.X < 0 ? -gamepadState.LeftStick.X / 100f : 0; var l1Button = gamepadState.Raw(GamepadButtons.L1) != 0; var l2Button = gamepadState.Raw(GamepadButtons.L2) != 0; var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index ce1559fc8..01d0b1759 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -18,7 +18,9 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// internal class NetworkMonitorWidget : IDataWindowWidget { +#pragma warning disable SA1313 private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId) +#pragma warning restore SA1313 { public readonly IReadOnlyList Data = Array.Empty(); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index db455b985..e73287ee9 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -87,18 +87,20 @@ internal class ProfileManagerWidget if (scrolling) { ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphOne); - ImGuiHelpers.ScaledDummy(3); + ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphTwo); - ImGuiHelpers.ScaledDummy(3); + ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphThree); - ImGuiHelpers.ScaledDummy(3); + ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphFour); - ImGuiHelpers.ScaledDummy(3); + ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommands); ImGui.BulletText(Locs.TutorialCommandsEnable); ImGui.BulletText(Locs.TutorialCommandsDisable); ImGui.BulletText(Locs.TutorialCommandsToggle); ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnd); + + ImGuiHelpers.ScaledDummy(5); var buttonWidth = 120f; ImGui.SetCursorPosX((ImGui.GetWindowWidth() - buttonWidth) / 2); @@ -564,9 +566,6 @@ internal class ProfileManagerWidget public static string ErrorCouldNotChangeState => Loc.Localize("ProfileManagerCouldNotChangeState", "Could not change plugin state."); - public static string NotInstalled(string name) => - Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name); - public static string TutorialTitle => Loc.Localize("ProfileManagerTutorial", "About Collections"); @@ -596,5 +595,8 @@ internal class ProfileManagerWidget public static string TutorialCommandsEnd => Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order."); + + public static string NotInstalled(string name) => + Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name); } } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/TargetAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/TargetAgingStep.cs index 1e66ac19e..0a1b4d91d 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/TargetAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/TargetAgingStep.cs @@ -23,8 +23,8 @@ internal class TargetAgingStep : IAgingStep switch (this.step) { case 0: - targetManager.ClearTarget(); - targetManager.ClearFocusTarget(); + targetManager.Target = null; + targetManager.FocusTarget = null; this.step++; diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index 69a3e9a21..fa8c5bf43 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; + using ImGuiScene; using Lumina; using Lumina.Data; From d6ab6146959dc6dc697f32b86ded9fdcacfaeaa1 Mon Sep 17 00:00:00 2001 From: Caraxi Date: Wed, 5 Jul 2023 18:31:53 +0930 Subject: [PATCH 48/81] Cache reflective name lookup Improve performance massively --- .../Game/Config/GameConfigEnumExtensions.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Config/GameConfigEnumExtensions.cs b/Dalamud/Game/Config/GameConfigEnumExtensions.cs index f880ee1b2..85d31a0aa 100644 --- a/Dalamud/Game/Config/GameConfigEnumExtensions.cs +++ b/Dalamud/Game/Config/GameConfigEnumExtensions.cs @@ -1,4 +1,6 @@ -using Dalamud.Utility; +using System.Collections.Generic; + +using Dalamud.Utility; namespace Dalamud.Game.Config; @@ -7,6 +9,10 @@ namespace Dalamud.Game.Config; /// internal static class GameConfigEnumExtensions { + private static readonly Dictionary SystemNameCache = new(); + private static readonly Dictionary UIConfigNameCache = new(); + private static readonly Dictionary UIControlNameCache = new(); + /// /// Gets the name of a SystemConfigOption from it's attribute. /// @@ -14,7 +20,10 @@ internal static class GameConfigEnumExtensions /// Name of the option. public static string GetName(this SystemConfigOption systemConfigOption) { - return systemConfigOption.GetAttribute()?.Name ?? $"{systemConfigOption}"; + if (SystemNameCache.TryGetValue(systemConfigOption, out var name)) return name; + name = systemConfigOption.GetAttribute()?.Name ?? $"{systemConfigOption}"; + SystemNameCache.TryAdd(systemConfigOption, name); + return name; } /// @@ -24,7 +33,10 @@ internal static class GameConfigEnumExtensions /// Name of the option. public static string GetName(this UiConfigOption uiConfigOption) { - return uiConfigOption.GetAttribute()?.Name ?? $"{uiConfigOption}"; + if (UIConfigNameCache.TryGetValue(uiConfigOption, out var name)) return name; + name = uiConfigOption.GetAttribute()?.Name ?? $"{uiConfigOption}"; + UIConfigNameCache.TryAdd(uiConfigOption, name); + return name; } /// @@ -34,6 +46,9 @@ internal static class GameConfigEnumExtensions /// Name of the option. public static string GetName(this UiControlOption uiControlOption) { - return uiControlOption.GetAttribute()?.Name ?? $"{uiControlOption}"; + if (UIControlNameCache.TryGetValue(uiControlOption, out var name)) return name; + name = uiControlOption.GetAttribute()?.Name ?? $"{uiControlOption}"; + UIControlNameCache.TryAdd(uiControlOption, name); + return name; } } From 9cd8c255cfb76882d1a3099d00c6139d07018704 Mon Sep 17 00:00:00 2001 From: Caraxi Date: Wed, 5 Jul 2023 20:08:08 +0930 Subject: [PATCH 49/81] Thread safety --- Dalamud/Game/Config/GameConfigEnumExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Config/GameConfigEnumExtensions.cs b/Dalamud/Game/Config/GameConfigEnumExtensions.cs index 85d31a0aa..69087350b 100644 --- a/Dalamud/Game/Config/GameConfigEnumExtensions.cs +++ b/Dalamud/Game/Config/GameConfigEnumExtensions.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; using Dalamud.Utility; @@ -9,9 +9,9 @@ namespace Dalamud.Game.Config; /// internal static class GameConfigEnumExtensions { - private static readonly Dictionary SystemNameCache = new(); - private static readonly Dictionary UIConfigNameCache = new(); - private static readonly Dictionary UIControlNameCache = new(); + private static readonly ConcurrentDictionary SystemNameCache = new(); + private static readonly ConcurrentDictionary UIConfigNameCache = new(); + private static readonly ConcurrentDictionary UIControlNameCache = new(); /// /// Gets the name of a SystemConfigOption from it's attribute. From 69137532ed44c40e730b058786851867e129de97 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Wed, 5 Jul 2023 13:58:45 -0700 Subject: [PATCH 50/81] Only Attempt Signing on Pushes (#1299) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e8c521195..7ada48e50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: - name: Test Dalamud run: .\build.ps1 test - name: Sign Dalamud - if: ${{ github.repository_owner == 'goatcorp' }} + if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} env: CODESIGN_CERT_PFX: ${{ secrets.CODESIGN_CERT_PFX }} CODESIGN_CERT_PASSWORD: ${{ secrets.CODESIGN_CERT_PASSWORD }} From e52f7696ba77c4032d8e9d1f3719769c936eb29e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:21:49 -0700 Subject: [PATCH 51/81] Add Open/Close SFX to Window (#1298) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Internal/DalamudConfiguration.cs | 6 +++++ .../Windows/Settings/Tabs/SettingsTabLook.cs | 6 +++++ Dalamud/Interface/Windowing/Window.cs | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index d192ab676..39c53c3cb 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -207,6 +207,12 @@ internal sealed class DalamudConfiguration : IServiceType /// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui. /// public bool IsDocking { get; set; } + + /// + /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. + /// This setting is effected by the in-game "System Sounds" option and volume. + /// + public bool EnablePluginUISoundEffects { get; set; } /// /// Gets or sets a value indicating whether viewports should always be disabled. diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 13adccffd..3e801a8c3 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -101,6 +101,12 @@ public class SettingsTabLook : SettingsTab Loc.Localize("DalamudSettingToggleDockingHint", "This will allow you to fuse and tab plugin windows."), c => c.IsDocking, (v, c) => c.IsDocking = v), + + new SettingsEntry( + Loc.Localize("DalamudSettingEnablePluginUISoundEffects", "Enable sound effects for plugin windows"), + Loc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."), + c => c.EnablePluginUISoundEffects, + (v, c) => c.EnablePluginUISoundEffects = v), new SettingsEntry( Loc.Localize("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"), diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 2a8beb639..3bf987690 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -2,6 +2,7 @@ using System.Numerics; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; namespace Dalamud.Interface.Windowing; @@ -16,6 +17,7 @@ public abstract class Window private bool internalLastIsOpen = false; private bool internalIsOpen = false; private bool nextFrameBringToFront = false; + private DalamudConfiguration Configuration; /// /// Initializes a new instance of the class. @@ -31,6 +33,7 @@ public abstract class Window this.WindowName = name; this.Flags = flags; this.ForceMainWindow = forceMainWindow; + this.Configuration = Service.Get(); } /// @@ -55,6 +58,21 @@ public abstract class Window /// public bool RespectCloseHotkey { get; set; } = true; + /// + /// Gets or sets a value indicating whether this window should not generate sound effects when opening and closing. + /// + public bool DisableWindowSounds { get; set; } = false; + + /// + /// Gets or sets a value representing the sound effect id to be played when the window is opened. + /// + public uint OnOpenSfxId { get; set; } = 23u; + + /// + /// Gets or sets a value representing the sound effect id to be played when the window is closed. + /// + public uint OnCloseSfxId { get; set; } = 24u; + /// /// Gets or sets the position of this window. /// @@ -219,6 +237,8 @@ public abstract class Window this.OnClose(); this.IsFocused = false; + + if (this.Configuration.EnablePluginUISoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnCloseSfxId, 0, 0, 0); } return; @@ -243,6 +263,8 @@ public abstract class Window { this.internalLastIsOpen = this.internalIsOpen; this.OnOpen(); + + if (this.Configuration.EnablePluginUISoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnOpenSfxId, 0, 0, 0); } var wasFocused = this.IsFocused; From 7109f21387fa02e856fed37b83974dfa208e3146 Mon Sep 17 00:00:00 2001 From: Cara Date: Thu, 6 Jul 2023 13:26:26 +0930 Subject: [PATCH 52/81] Config change event (#1301) * Add events for config changes * Update ConfigChangeEvent.cs * change event names --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- Dalamud/Game/Config/ConfigChangeEvent.cs | 7 +++ Dalamud/Game/Config/GameConfig.cs | 61 ++++++++++++++++++- .../Game/Config/GameConfigAddressResolver.cs | 18 ++++++ Dalamud/Game/Config/GameConfigSection.cs | 41 ++++++++++++- Dalamud/Plugin/Services/IGameConfig.cs | 9 ++- 5 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 Dalamud/Game/Config/ConfigChangeEvent.cs create mode 100644 Dalamud/Game/Config/GameConfigAddressResolver.cs diff --git a/Dalamud/Game/Config/ConfigChangeEvent.cs b/Dalamud/Game/Config/ConfigChangeEvent.cs new file mode 100644 index 000000000..941033c61 --- /dev/null +++ b/Dalamud/Game/Config/ConfigChangeEvent.cs @@ -0,0 +1,7 @@ +using System; + +namespace Dalamud.Game.Config; + +public abstract record ConfigChangeEvent(Enum Option); + +public record ConfigChangeEvent(T ConfigOption) : ConfigChangeEvent(ConfigOption) where T : Enum; diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 5587787c9..a41b60936 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -1,6 +1,10 @@ -using Dalamud.IoC; +using System; +using Dalamud.Hooking; +using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Common.Configuration; using Serilog; namespace Dalamud.Game.Config; @@ -14,10 +18,13 @@ namespace Dalamud.Game.Config; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class GameConfig : IServiceType, IGameConfig +public sealed class GameConfig : IServiceType, IGameConfig, IDisposable { + private readonly GameConfigAddressResolver address = new(); + private Hook? configChangeHook; + [ServiceManager.ServiceConstructor] - private unsafe GameConfig(Framework framework) + private unsafe GameConfig(Framework framework, SigScanner sigScanner) { framework.RunOnTick(() => { @@ -27,9 +34,18 @@ public sealed class GameConfig : IServiceType, IGameConfig this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase); this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig); this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig); + + this.address.Setup(sigScanner); + this.configChangeHook = Hook.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged); + this.configChangeHook?.Enable(); }); } + private unsafe delegate nint ConfigChangeDelegate(ConfigBase* configBase, ConfigEntry* configEntry); + + /// + public event EventHandler Changed; + /// public GameConfigSection System { get; private set; } @@ -110,4 +126,43 @@ public sealed class GameConfig : IServiceType, IGameConfig /// public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value); + + /// + void IDisposable.Dispose() + { + this.configChangeHook?.Disable(); + this.configChangeHook?.Dispose(); + } + + private unsafe nint OnConfigChanged(ConfigBase* configBase, ConfigEntry* configEntry) + { + var returnValue = this.configChangeHook!.Original(configBase, configEntry); + try + { + ConfigChangeEvent? eventArgs = null; + + if (configBase == this.System.GetConfigBase()) + { + eventArgs = this.System.InvokeChange(configEntry); + } + else if (configBase == this.UiConfig.GetConfigBase()) + { + eventArgs = this.UiConfig.InvokeChange(configEntry); + } + else if (configBase == this.UiControl.GetConfigBase()) + { + eventArgs = this.UiControl.InvokeChange(configEntry); + } + + if (eventArgs == null) return returnValue; + + this.Changed?.InvokeSafely(this, eventArgs); + } + catch (Exception ex) + { + Log.Error(ex, $"Exception thrown handing {nameof(this.OnConfigChanged)} events."); + } + + return returnValue; + } } diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs new file mode 100644 index 000000000..6a207807a --- /dev/null +++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Config; + +/// +/// Game config system address resolver. +/// +public sealed class GameConfigAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the method called when any config option is changed. + /// + public nint ConfigChangeAddress { get; private set; } + + /// + protected override void Setup64Bit(SigScanner scanner) + { + this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E"); + } +} diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs index 107b0d4a8..7b2751901 100644 --- a/Dalamud/Game/Config/GameConfigSection.cs +++ b/Dalamud/Game/Config/GameConfigSection.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using Dalamud.Memory; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Common.Configuration; using Serilog; @@ -16,6 +17,12 @@ public class GameConfigSection private readonly Framework framework; private readonly Dictionary indexMap = new(); private readonly Dictionary nameMap = new(); + private readonly Dictionary enumMap = new(); + + /// + /// Event which is fired when a game config option is changed within the section. + /// + public event EventHandler Changed; /// /// Initializes a new instance of the class. @@ -59,7 +66,10 @@ public class GameConfigSection /// public string SectionName { get; } - private GetConfigBaseDelegate GetConfigBase { get; } + /// + /// Gets the pointer to the config section container. + /// + internal GetConfigBaseDelegate GetConfigBase { get; } /// /// Attempts to get a boolean config option. @@ -380,6 +390,35 @@ public class GameConfigSection }); } + /// + /// Invokes a change event within the config section. + /// + /// The config entry that was changed. + /// SystemConfigOption, UiConfigOption, or UiControlOption. + /// The ConfigChangeEvent record. + internal unsafe ConfigChangeEvent? InvokeChange(ConfigEntry* entry) where TEnum : Enum + { + if (!this.enumMap.TryGetValue(entry->Index, out var enumObject)) + { + if (entry->Name == null) return null; + var name = MemoryHelper.ReadStringNullTerminated(new IntPtr(entry->Name)); + if (Enum.TryParse(typeof(TEnum), name, out enumObject)) + { + this.enumMap.Add(entry->Index, enumObject); + } + else + { + enumObject = null; + this.enumMap.Add(entry->Index, null); + } + } + + if (enumObject == null) return null; + var eventArgs = new ConfigChangeEvent((TEnum)enumObject); + this.Changed?.InvokeSafely(this, eventArgs); + return eventArgs; + } + private unsafe bool TryGetIndex(string name, out uint index) { if (this.indexMap.TryGetValue(name, out index)) diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index bbff123c0..f0607c39e 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -1,6 +1,8 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using Dalamud.Game.Config; +using FFXIVClientStructs.FFXIV.Common.Configuration; namespace Dalamud.Plugin.Services; @@ -9,6 +11,11 @@ namespace Dalamud.Plugin.Services; /// public interface IGameConfig { + /// + /// Event which is fired when a game config option is changed. + /// + public event EventHandler Changed; + /// /// Gets the collection of config options that persist between characters. /// From 86b79c1df4ceb37b69aea7c581072b37dd2d1b94 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 8 Jul 2023 12:34:04 +0200 Subject: [PATCH 53/81] [master] Update ClientStructs (#1306) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 20cdc0e0f..93db21d9b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 20cdc0e0fab656bb98b34f593fefdfe15b9df380 +Subproject commit 93db21d9b6fb5cc671cd25c79a6ac933f3ca6710 From c0715515745bf026dc353e4d405b9de4ffa49d3e Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 8 Jul 2023 12:39:02 +0200 Subject: [PATCH 54/81] remove unneeded includes --- Dalamud/Dalamud.cs | 1 - Dalamud/Interface/UiBuilder.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 4e491f12a..c38594771 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.Gui.Internal; -using Dalamud.Interface.DragDrop; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 53f223ef2..eca0f64a0 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -7,7 +7,6 @@ using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; -using Dalamud.Interface.DragDrop; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; From 1b46bbac8786d4b6d064ddbf0ed6cdeada670d5b Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 8 Jul 2023 15:01:38 +0200 Subject: [PATCH 55/81] build: 7.8.0.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index e93c0c25a..fbc9c1149 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.7.5.0 + 7.8.0.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 49536105056a4a9704e720f1237126a1727d6497 Mon Sep 17 00:00:00 2001 From: Caraxi Date: Sun, 9 Jul 2023 10:43:42 +0930 Subject: [PATCH 56/81] GameConfig - Fix incorrect overload mapping --- Dalamud/Game/Config/GameConfig.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index a41b60936..db3163a19 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -77,7 +77,7 @@ public sealed class GameConfig : IServiceType, IGameConfig, IDisposable public bool TryGet(UiConfigOption option, out float value) => this.UiConfig.TryGet(option.GetName(), out value); /// - public bool TryGet(UiConfigOption option, out string value) => this.UiControl.TryGet(option.GetName(), out value); + public bool TryGet(UiConfigOption option, out string value) => this.UiConfig.TryGet(option.GetName(), out value); /// public bool TryGet(UiControlOption option, out bool value) => this.UiControl.TryGet(option.GetName(), out value); @@ -89,7 +89,7 @@ public sealed class GameConfig : IServiceType, IGameConfig, IDisposable public bool TryGet(UiControlOption option, out float value) => this.UiControl.TryGet(option.GetName(), out value); /// - public bool TryGet(UiControlOption option, out string value) => this.System.TryGet(option.GetName(), out value); + public bool TryGet(UiControlOption option, out string value) => this.UiControl.TryGet(option.GetName(), out value); /// public void Set(SystemConfigOption option, bool value) => this.System.Set(option.GetName(), value); From c4f8a095303afaea65aba4cb2a40406e9882025d Mon Sep 17 00:00:00 2001 From: Caraxi Date: Sun, 9 Jul 2023 10:44:56 +0930 Subject: [PATCH 57/81] GameConfig - Use ConcurrentDictionary for index cache --- Dalamud/Game/Config/GameConfigSection.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs index 7b2751901..a3542ba3e 100644 --- a/Dalamud/Game/Config/GameConfigSection.cs +++ b/Dalamud/Game/Config/GameConfigSection.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Diagnostics; using Dalamud.Memory; @@ -15,9 +15,8 @@ namespace Dalamud.Game.Config; public class GameConfigSection { private readonly Framework framework; - private readonly Dictionary indexMap = new(); - private readonly Dictionary nameMap = new(); - private readonly Dictionary enumMap = new(); + private readonly ConcurrentDictionary indexMap = new(); + private readonly ConcurrentDictionary enumMap = new(); /// /// Event which is fired when a game config option is changed within the section. @@ -404,12 +403,12 @@ public class GameConfigSection var name = MemoryHelper.ReadStringNullTerminated(new IntPtr(entry->Name)); if (Enum.TryParse(typeof(TEnum), name, out enumObject)) { - this.enumMap.Add(entry->Index, enumObject); + this.enumMap.TryAdd(entry->Index, enumObject); } else { enumObject = null; - this.enumMap.Add(entry->Index, null); + this.enumMap.TryAdd(entry->Index, null); } } @@ -439,7 +438,6 @@ public class GameConfigSection if (eName.Equals(name)) { this.indexMap.TryAdd(name, i); - this.nameMap.TryAdd(i, name); index = i; return true; } From 4dc43b7ed3da275dd349c28833a8bfa0ca5a267e Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 10 Jul 2023 22:31:37 +0200 Subject: [PATCH 58/81] feat: assign every installation of a plugin a unique id, to be used to differentiate between them in the future(api9+) --- .../PluginInstaller/PluginInstallerWindow.cs | 8 ++++++++ Dalamud/Plugin/Internal/PluginManager.cs | 17 +++++++++++++++-- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 8 ++++++++ .../Types/Manifest/ILocalPluginManifest.cs | 9 ++++++++- .../Types/Manifest/LocalPluginManifest.cs | 3 +++ 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 05376a770..f21925b5a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2186,6 +2186,14 @@ internal class PluginInstallerWindow : Window, IDisposable ImGuiHelpers.SafeTextWrapped(manifest.Description); } + // Working Plugin ID + if (this.hasDevPlugins) + { + ImGuiHelpers.ScaledDummy(3); + ImGui.TextColored(ImGuiColors.DalamudGrey, $"WorkingPluginId: {manifest.WorkingPluginId}"); + ImGuiHelpers.ScaledDummy(3); + } + // Available commands (if loaded) if (plugin.IsLoaded) { diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index c98a6bbfc..58e122c3e 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -763,8 +763,9 @@ internal partial class PluginManager : IDisposable, IServiceType /// The plugin definition. /// If the testing version should be used. /// The reason this plugin was loaded. + /// WorkingPluginId this plugin should inherit. /// A representing the asynchronous operation. - public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason) + public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason, Guid? inheritedWorkingPluginId = null) { Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})"); @@ -851,6 +852,9 @@ internal partial class PluginManager : IDisposable, IServiceType // Reload as a local manifest, add some attributes, and save again. var manifest = LocalPluginManifest.Load(manifestFile); + if (manifest == null) + throw new Exception("Plugin had no valid manifest"); + if (manifest.InternalName != repoManifest.InternalName) { Directory.Delete(outputDir.FullName, true); @@ -858,6 +862,11 @@ internal partial class PluginManager : IDisposable, IServiceType $"Distributed internal name does not match repo internal name: {manifest.InternalName} - {repoManifest.InternalName}"); } + if (manifest.WorkingPluginId != Guid.Empty) + throw new Exception("Plugin shall not specify a WorkingPluginId"); + + manifest.WorkingPluginId = inheritedWorkingPluginId ?? Guid.NewGuid(); + if (useTesting) { manifest.Testing = true; @@ -1040,6 +1049,10 @@ internal partial class PluginManager : IDisposable, IServiceType { var plugin = metadata.InstalledPlugin; + var workingPluginId = metadata.InstalledPlugin.Manifest.WorkingPluginId; + if (workingPluginId == Guid.Empty) + throw new Exception("Existing plugin had no WorkingPluginId"); + var updateStatus = new PluginUpdateStatus { InternalName = plugin.Manifest.InternalName, @@ -1099,7 +1112,7 @@ internal partial class PluginManager : IDisposable, IServiceType try { - await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update); + await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update, workingPluginId); } catch (Exception ex) { diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 5ad09ebc3..115ab0f8d 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -158,6 +158,14 @@ internal class LocalPlugin : IDisposable needsSaveDueToLegacyFiles = true; } + // Create an installation instance ID for this plugin, if it doesn't have one yet + if (this.manifest.WorkingPluginId == Guid.Empty) + { + this.manifest.WorkingPluginId = Guid.NewGuid(); + + needsSaveDueToLegacyFiles = true; + } + var pluginManager = Service.Get(); this.IsBanned = pluginManager.IsManifestBanned(this.manifest) && !this.IsDev; this.BanReason = pluginManager.GetBanReason(this.manifest); diff --git a/Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs index 97365d1e5..5b147dde1 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Plugin.Internal.Types.Manifest; +using System; + +namespace Dalamud.Plugin.Internal.Types.Manifest; /// /// Public interface for the local plugin manifest. @@ -16,4 +18,9 @@ public interface ILocalPluginManifest : IPluginManifest /// Gets a value indicating whether the plugin should be deleted during the next cleanup. /// public bool ScheduledForDeletion { get; } + + /// + /// Gets an ID uniquely identifying this specific installation of a plugin. + /// + public Guid WorkingPluginId { get; } } diff --git a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs index 4b4bf5d6e..8afbe1aea 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs @@ -44,6 +44,9 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest /// public string InstalledFromUrl { get; set; } = string.Empty; + /// + public Guid WorkingPluginId { get; set; } = Guid.Empty; + /// /// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party /// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null. From 4befb44c907f683c911b93df6141aade0354ea22 Mon Sep 17 00:00:00 2001 From: Caraxi Date: Tue, 11 Jul 2023 17:03:41 +0930 Subject: [PATCH 59/81] GameConfig - add methods to get properties --- Dalamud/Game/Config/GameConfig.cs | 27 +++++++ Dalamud/Game/Config/GameConfigSection.cs | 93 ++++++++++++++++++++++++ Dalamud/Game/Config/Properties.cs | 7 ++ Dalamud/Plugin/Services/IGameConfig.cs | 72 ++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 Dalamud/Game/Config/Properties.cs diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index db3163a19..dfdb8b5d2 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -67,6 +67,15 @@ public sealed class GameConfig : IServiceType, IGameConfig, IDisposable /// public bool TryGet(SystemConfigOption option, out string value) => this.System.TryGet(option.GetName(), out value); + /// + public bool TryGet(SystemConfigOption option, out UIntConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties); + + /// + public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties); + + /// + public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties); + /// public bool TryGet(UiConfigOption option, out bool value) => this.UiConfig.TryGet(option.GetName(), out value); @@ -78,6 +87,15 @@ public sealed class GameConfig : IServiceType, IGameConfig, IDisposable /// public bool TryGet(UiConfigOption option, out string value) => this.UiConfig.TryGet(option.GetName(), out value); + + /// + public bool TryGet(UiConfigOption option, out UIntConfigProperties properties) => this.UiConfig.TryGetProperties(option.GetName(), out properties); + + /// + public bool TryGet(UiConfigOption option, out FloatConfigProperties properties) => this.UiConfig.TryGetProperties(option.GetName(), out properties); + + /// + public bool TryGet(UiConfigOption option, out StringConfigProperties properties) => this.UiConfig.TryGetProperties(option.GetName(), out properties); /// public bool TryGet(UiControlOption option, out bool value) => this.UiControl.TryGet(option.GetName(), out value); @@ -90,7 +108,16 @@ public sealed class GameConfig : IServiceType, IGameConfig, IDisposable /// public bool TryGet(UiControlOption option, out string value) => this.UiControl.TryGet(option.GetName(), out value); + + /// + public bool TryGet(UiControlOption option, out UIntConfigProperties properties) => this.UiControl.TryGetProperties(option.GetName(), out properties); + /// + public bool TryGet(UiControlOption option, out FloatConfigProperties properties) => this.UiControl.TryGetProperties(option.GetName(), out properties); + + /// + public bool TryGet(UiControlOption option, out StringConfigProperties properties) => this.UiControl.TryGetProperties(option.GetName(), out properties); + /// public void Set(SystemConfigOption option, bool value) => this.System.Set(option.GetName(), value); diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs index a3542ba3e..6c87ad3cf 100644 --- a/Dalamud/Game/Config/GameConfigSection.cs +++ b/Dalamud/Game/Config/GameConfigSection.cs @@ -389,6 +389,99 @@ public class GameConfigSection }); } + /// + /// Attempts to get the properties of a UInt option from the config section. + /// + /// Name of the option to get the properties of. + /// Details of the option: Minimum, Maximum, and Default values. + /// A value representing the success. + public unsafe bool TryGetProperties(string name, out UIntConfigProperties? properties) + { + if (!this.TryGetIndex(name, out var index)) + { + properties = null; + return false; + } + + if (!this.TryGetEntry(index, out var entry)) + { + properties = null; + return false; + } + + if ((ConfigType)entry->Type != ConfigType.UInt) + { + properties = null; + return false; + } + + var prop = &entry->Properties.UInt; + properties = new UIntConfigProperties(prop->DefaultValue, prop->MinValue, prop->MaxValue); + return true; + } + + /// + /// Attempts to get the properties of a Float option from the config section. + /// + /// Name of the option to get the properties of. + /// Details of the option: Minimum, Maximum, and Default values. + /// A value representing the success. + public unsafe bool TryGetProperties(string name, out FloatConfigProperties? properties) + { + if (!this.TryGetIndex(name, out var index)) + { + properties = null; + return false; + } + + if (!this.TryGetEntry(index, out var entry)) + { + properties = null; + return false; + } + + if ((ConfigType)entry->Type != ConfigType.Float) + { + properties = null; + return false; + } + + var prop = &entry->Properties.Float; + properties = new FloatConfigProperties(prop->DefaultValue, prop->MinValue, prop->MaxValue); + return true; + } + + /// + /// Attempts to get the properties of a String option from the config section. + /// + /// Name of the option to get the properties of. + /// Details of the option: Minimum, Maximum, and Default values. + /// A value representing the success. + public unsafe bool TryGetProperties(string name, out StringConfigProperties? properties) + { + if (!this.TryGetIndex(name, out var index)) + { + properties = null; + return false; + } + + if (!this.TryGetEntry(index, out var entry)) + { + properties = null; + return false; + } + + if ((ConfigType)entry->Type != ConfigType.String) + { + properties = null; + return false; + } + + var prop = entry->Properties.String; + properties = new StringConfigProperties(prop.DefaultValue == null ? null : MemoryHelper.ReadSeString(prop.DefaultValue)); + return true; + } + /// /// Invokes a change event within the config section. /// diff --git a/Dalamud/Game/Config/Properties.cs b/Dalamud/Game/Config/Properties.cs new file mode 100644 index 000000000..b43a44a47 --- /dev/null +++ b/Dalamud/Game/Config/Properties.cs @@ -0,0 +1,7 @@ +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Game.Config; + +public record StringConfigProperties(SeString? Default); +public record UIntConfigProperties(uint Default, uint Minimum, uint Maximum); +public record FloatConfigProperties(float Default, float Minimum, float Maximum); diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index f0607c39e..98f6160cc 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -62,6 +62,30 @@ public interface IGameConfig /// The returned value of the config option. /// A value representing the success. public bool TryGet(SystemConfigOption option, out string value); + + /// + /// Attempts to get the properties of a UInt option from the System section. + /// + /// Option to get the properties of. + /// Details of the option: Minimum, Maximum, and Default values. + /// A value representing the success. + public bool TryGet(SystemConfigOption option, out UIntConfigProperties? properties); + + /// + /// Attempts to get the properties of a Float option from the System section. + /// + /// Option to get the properties of. + /// Details of the option: Minimum, Maximum, and Default values. + /// A value representing the success. + public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties); + + /// + /// Attempts to get the properties of a String option from the System section. + /// + /// Option to get the properties of. + /// Details of the option: Default Value + /// A value representing the success. + public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties); /// /// Attempts to get a boolean config value from the UiConfig section. @@ -95,6 +119,30 @@ public interface IGameConfig /// A value representing the success. public bool TryGet(UiConfigOption option, out string value); + /// + /// Attempts to get the properties of a UInt option from the UiConfig section. + /// + /// Option to get the properties of. + /// Details of the option: Minimum, Maximum, and Default values. + /// A value representing the success. + public bool TryGet(UiConfigOption option, out UIntConfigProperties? properties); + + /// + /// Attempts to get the properties of a Float option from the UiConfig section. + /// + /// Option to get the properties of. + /// Details of the option: Minimum, Maximum, and Default values. + /// A value representing the success. + public bool TryGet(UiConfigOption option, out FloatConfigProperties? properties); + + /// + /// Attempts to get the properties of a String option from the UiConfig section. + /// + /// Option to get the properties of. + /// Details of the option: Default Value + /// A value representing the success. + public bool TryGet(UiConfigOption option, out StringConfigProperties? properties); + /// /// Attempts to get a boolean config value from the UiControl section. /// @@ -127,6 +175,30 @@ public interface IGameConfig /// A value representing the success. public bool TryGet(UiControlOption option, out string value); + /// + /// Attempts to get the properties of a UInt option from the UiControl section. + /// + /// Option to get the properties of. + /// Details of the option: Minimum, Maximum, and Default values. + /// A value representing the success. + public bool TryGet(UiControlOption option, out UIntConfigProperties? properties); + + /// + /// Attempts to get the properties of a Float option from the UiControl section. + /// + /// Option to get the properties of. + /// Details of the option: Minimum, Maximum, and Default values. + /// A value representing the success. + public bool TryGet(UiControlOption option, out FloatConfigProperties? properties); + + /// + /// Attempts to get the properties of a String option from the UiControl section. + /// + /// Option to get the properties of. + /// Details of the option: Default Value + /// A value representing the success. + public bool TryGet(UiControlOption option, out StringConfigProperties? properties); + /// /// Set a boolean config option in the System config section. /// Note: Not all config options will be be immediately reflected in the game. From a82096c671259294b658e0a09d653457267a597b Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 11 Jul 2023 20:54:59 +0200 Subject: [PATCH 60/81] fix: allow Window to be instantiated without Dalamud services --- Dalamud/Interface/Windowing/Window.cs | 31 +++++++++++---------- Dalamud/Interface/Windowing/WindowSystem.cs | 5 +++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 3bf987690..39c61566b 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -17,7 +17,6 @@ public abstract class Window private bool internalLastIsOpen = false; private bool internalIsOpen = false; private bool nextFrameBringToFront = false; - private DalamudConfiguration Configuration; /// /// Initializes a new instance of the class. @@ -33,7 +32,6 @@ public abstract class Window this.WindowName = name; this.Flags = flags; this.ForceMainWindow = forceMainWindow; - this.Configuration = Service.Get(); } /// @@ -225,10 +223,12 @@ public abstract class Window /// /// Draw the window via ImGui. /// - internal void DrawInternal() + internal void DrawInternal(DalamudConfiguration? configuration) { this.PreOpenCheck(); + var doSoundEffects = configuration?.EnablePluginUISoundEffects ?? false; + if (!this.IsOpen) { if (this.internalIsOpen != this.internalLastIsOpen) @@ -238,7 +238,7 @@ public abstract class Window this.IsFocused = false; - if (this.Configuration.EnablePluginUISoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnCloseSfxId, 0, 0, 0); + if (doSoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnCloseSfxId, 0, 0, 0); } return; @@ -264,7 +264,7 @@ public abstract class Window this.internalLastIsOpen = this.internalIsOpen; this.OnOpen(); - if (this.Configuration.EnablePluginUISoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnOpenSfxId, 0, 0, 0); + if (doSoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnOpenSfxId, 0, 0, 0); } var wasFocused = this.IsFocused; @@ -294,16 +294,19 @@ public abstract class Window this.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows); - var escapeDown = Service.Get()[VirtualKey.ESCAPE]; - var isAllowed = Service.Get().IsFocusManagementEnabled; - if (escapeDown && this.IsFocused && isAllowed && !wasEscPressedLastFrame && this.RespectCloseHotkey) + var isAllowed = configuration?.IsFocusManagementEnabled ?? false; + if (isAllowed) { - this.IsOpen = false; - wasEscPressedLastFrame = true; - } - else if (!escapeDown && wasEscPressedLastFrame) - { - wasEscPressedLastFrame = false; + var escapeDown = Service.Get()[VirtualKey.ESCAPE]; + if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey) + { + this.IsOpen = false; + wasEscPressedLastFrame = true; + } + else if (!escapeDown && wasEscPressedLastFrame) + { + wasEscPressedLastFrame = false; + } } ImGui.End(); diff --git a/Dalamud/Interface/Windowing/WindowSystem.cs b/Dalamud/Interface/Windowing/WindowSystem.cs index 516f3c21b..8e12d8f68 100644 --- a/Dalamud/Interface/Windowing/WindowSystem.cs +++ b/Dalamud/Interface/Windowing/WindowSystem.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Configuration.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using ImGuiNET; using Serilog; @@ -111,6 +112,8 @@ public class WindowSystem if (hasNamespace) ImGui.PushID(this.Namespace); + var config = Service.GetNullable(); + // Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating foreach (var window in this.windows.ToArray()) { @@ -119,7 +122,7 @@ public class WindowSystem #endif var snapshot = ImGuiManagedAsserts.GetSnapshot(); - window.DrawInternal(); + window.DrawInternal(config); var source = ($"{this.Namespace}::" ?? string.Empty) + window.WindowName; ImGuiManagedAsserts.ReportProblems(source, snapshot); From 00a883a8e2f87e464a83196f5ebcdd1d60716f72 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 11 Jul 2023 19:59:09 -0700 Subject: [PATCH 61/81] Fixes all fatetable copy buttons having the same ID --- .../Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs index 78a93c1cc..779032f1d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs @@ -58,7 +58,7 @@ internal class FateTableWidget : IDataWindowWidget ImGui.TextUnformatted(fateString); ImGui.SameLine(); - if (ImGui.Button("C")) + if (ImGui.Button($"C##{fate.Address.ToInt64():X}")) { ImGui.SetClipboardText(fate.Address.ToString("X")); } From 1f0526b2ef6f4fd1cd8330bd7ed6293e5ca2621c Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jul 2023 13:29:16 +0200 Subject: [PATCH 62/81] fix: make CommandManager thread-safe --- Dalamud/Game/Command/CommandManager.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 11cbbffbd..63a1a3d09 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text.RegularExpressions; @@ -24,7 +25,7 @@ namespace Dalamud.Game.Command; #pragma warning restore SA1015 public sealed class CommandManager : IServiceType, IDisposable, ICommandManager { - private readonly Dictionary commandMap = new(); + private readonly ConcurrentDictionary commandMap = new(); private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled); private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?.+)$", RegexOptions.Compiled); private readonly Regex commandRegexDe = new(@"^„(?.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled); @@ -115,22 +116,19 @@ public sealed class CommandManager : IServiceType, IDisposable, ICommandManager if (info == null) throw new ArgumentNullException(nameof(info), "Command handler is null."); - try - { - this.commandMap.Add(command, info); - return true; - } - catch (ArgumentException) + if (!this.commandMap.TryAdd(command, info)) { Log.Error("Command {CommandName} is already registered.", command); return false; } + + return true; } /// public bool RemoveHandler(string command) { - return this.commandMap.Remove(command); + return this.commandMap.Remove(command, out _); } /// From af157c36646c8be396dad57a59f6e849e9a5ebae Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jul 2023 22:46:47 +0200 Subject: [PATCH 63/81] fix: only persist plugin state to default profile when removing profile is enabled --- Dalamud/Plugin/Internal/Profiles/Profile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 71feff0c2..61d521e89 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -219,7 +219,7 @@ internal class Profile { if (!this.IsDefaultProfile) { - await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, entry.IsEnabled, false); + await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, this.IsEnabled && entry.IsEnabled, false); } else { From 1a5bd88b2ccb7bfdbcfad8dc5e30c207a6d1cd43 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jul 2023 00:35:57 +0200 Subject: [PATCH 64/81] Fix drag & drop tooltip persisting when moving outside the viewports. --- Dalamud/Interface/DragDrop/DragDropTarget.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index 05e5599f9..5e7166fb3 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -78,6 +78,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget this.Files = Array.Empty(); this.Directories = Array.Empty(); this.Extensions = new HashSet(); + MouseDrop(this.lastKeyState); Log.Debug("[DragDrop] Leaving external Drag and Drop."); } From be9ca7618aadc4ae101c5704198e900f381dab24 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jul 2023 00:47:16 +0200 Subject: [PATCH 65/81] Maybe fix dependency issues with early loading plugins that request IDragDropManager? --- Dalamud/Interface/DragDrop/DragDropManager.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 34f1296e1..8336edc11 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -19,15 +19,21 @@ namespace Dalamud.Interface.DragDrop; [ResolveVia] internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType { - [ServiceManager.ServiceDependency] - private readonly InterfaceManager.InterfaceManagerWithScene interfaceManager = Service.Get(); + private nint windowHandlePtr = nint.Zero; private int lastDropFrame = -2; private int lastTooltipFrame = -1; [ServiceManager.ServiceConstructor] private DragDropManager() - => this.Enable(); + { + Service.GetAsync() + .ContinueWith(t => + { + this.windowHandlePtr = t.Result.Manager.WindowHandlePtr; + this.Enable(); + }); + } /// Gets a value indicating whether external drag and drop is available at all. public bool ServiceAvailable { get; private set; } @@ -51,21 +57,21 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService /// Enable external drag and drop. public void Enable() { - if (this.ServiceAvailable) + if (this.ServiceAvailable || this.windowHandlePtr == nint.Zero) { return; } try { - var ret = DragDropInterop.RegisterDragDrop(this.interfaceManager.Manager.WindowHandlePtr, this); - Log.Information($"[DragDrop] Registered window 0x{this.interfaceManager.Manager.WindowHandlePtr:X} for external drag and drop operations. ({ret})"); + var ret = DragDropInterop.RegisterDragDrop(this.windowHandlePtr, this); + Log.Information($"[DragDrop] Registered window 0x{this.windowHandlePtr:X} for external drag and drop operations. ({ret})"); Marshal.ThrowExceptionForHR(ret); this.ServiceAvailable = true; } catch (Exception ex) { - Log.Error($"Could not create windows drag and drop utility for window 0x{this.interfaceManager.Manager.WindowHandlePtr:X}:\n{ex}"); + Log.Error($"Could not create windows drag and drop utility for window 0x{this.windowHandlePtr:X}:\n{ex}"); } } @@ -79,13 +85,13 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService try { - var ret = DragDropInterop.RevokeDragDrop(this.interfaceManager.Manager.WindowHandlePtr); - Log.Information($"[DragDrop] Disabled external drag and drop operations for window 0x{this.interfaceManager.Manager.WindowHandlePtr:X}. ({ret})"); + var ret = DragDropInterop.RevokeDragDrop(this.windowHandlePtr); + Log.Information($"[DragDrop] Disabled external drag and drop operations for window 0x{this.windowHandlePtr:X}. ({ret})"); Marshal.ThrowExceptionForHR(ret); } catch (Exception ex) { - Log.Error($"Could not disable windows drag and drop utility for window 0x{this.interfaceManager.Manager.WindowHandlePtr:X}:\n{ex}"); + Log.Error($"Could not disable windows drag and drop utility for window 0x{this.windowHandlePtr:X}:\n{ex}"); } this.ServiceAvailable = false; From bbdd1ef601fbe0f4653f1738a55950c3d6e86aa1 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 18 Jul 2023 16:53:00 +0200 Subject: [PATCH 66/81] Update ClientStructs (#1309) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 93db21d9b..7092f5046 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 93db21d9b6fb5cc671cd25c79a6ac933f3ca6710 +Subproject commit 7092f5046832be09ec559c64087336710cc10873 From fd08fb7e001e8583b63bb81647ff300a01805732 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 18 Jul 2023 16:54:58 +0200 Subject: [PATCH 67/81] Update DalamudPackager in Dalamud.Plugin.targets (#1311) --- targets/Dalamud.Plugin.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets index 9234f5064..74dd4d8c0 100644 --- a/targets/Dalamud.Plugin.targets +++ b/targets/Dalamud.Plugin.targets @@ -13,7 +13,7 @@ - + $(DalamudLibPath)FFXIVClientStructs.dll false From 884935e2b4a9769835cdbfd22fcdd97c8d6cd6cc Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 18 Jul 2023 16:55:08 +0200 Subject: [PATCH 68/81] Remove Sdk attribute from targets (#1312) --- targets/Dalamud.Plugin.Bootstrap.targets | 2 +- targets/Dalamud.Plugin.targets | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/targets/Dalamud.Plugin.Bootstrap.targets b/targets/Dalamud.Plugin.Bootstrap.targets index 16f78517c..c30a5acba 100644 --- a/targets/Dalamud.Plugin.Bootstrap.targets +++ b/targets/Dalamud.Plugin.Bootstrap.targets @@ -1,5 +1,5 @@ - + $(appdata)\XIVLauncher\addon\Hooks\dev\ diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets index 74dd4d8c0..a6a2faf48 100644 --- a/targets/Dalamud.Plugin.targets +++ b/targets/Dalamud.Plugin.targets @@ -1,5 +1,5 @@ - + net7.0-windows x64 From 0f2b85803829688557f388f7a90beb5fc2feed89 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 18 Jul 2023 17:09:46 +0200 Subject: [PATCH 69/81] Add DalamudLibPath to AssemblySearchPaths (#1313) --- targets/Dalamud.Plugin.targets | 41 ++++++++-------------------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets index a6a2faf48..4a5f9e97e 100644 --- a/targets/Dalamud.Plugin.targets +++ b/targets/Dalamud.Plugin.targets @@ -10,42 +10,19 @@ false true true + $(AssemblySearchPaths);$(DalamudLibPath) - - $(DalamudLibPath)FFXIVClientStructs.dll - false - - - $(DalamudLibPath)Newtonsoft.Json.dll - false - - - $(DalamudLibPath)Dalamud.dll - false - - - $(DalamudLibPath)Dalamud.Interface.dll - false - - - $(DalamudLibPath)ImGui.NET.dll - false - - - $(DalamudLibPath)ImGuiScene.dll - false - - - $(DalamudLibPath)Lumina.dll - false - - - $(DalamudLibPath)Lumina.Excel.dll - false - + + + + + + + + From 3da22fca81043c3ae66f521445029814529b0cd9 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:20:01 +0200 Subject: [PATCH 70/81] Update ClientStructs (#1315) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 7092f5046..07bae81be 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7092f5046832be09ec559c64087336710cc10873 +Subproject commit 07bae81bede9923ec021b85f6119be394b93a757 From 8c57f26a18241ca97ea4243620eb81ed39bb00d7 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 18 Jul 2023 20:24:54 +0200 Subject: [PATCH 71/81] chore: disable profiles for dev plugins for now, it's not functional --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- .../Internal/Windows/PluginInstaller/ProfileManagerWidget.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index f21925b5a..d3779ba3d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2350,7 +2350,7 @@ internal class PluginInstallerWindow : Window, IDisposable var profileManager = Service.Get(); var config = Service.Get(); - var applicableForProfiles = plugin.Manifest.SupportsProfiles; + var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev; var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName); // Disable everything if the updater is running or another plugin is operating diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index e73287ee9..074ccdf32 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -282,7 +282,7 @@ internal class ProfileManagerWidget if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80))) { // TODO: Plugin searching should be abstracted... installer and this should use the same search - foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && + foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && !x.IsDev && (this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant())))) { using var disabled2 = From ea8b7ed0b3e006295d2be8849a2717eb05ec962e Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 18 Jul 2023 20:28:48 +0200 Subject: [PATCH 72/81] feat: add sort for in a profile/not in a profile --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index d3779ba3d..f87e46855 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -184,6 +184,7 @@ internal class PluginInstallerWindow : Window, IDisposable NewOrNot, NotInstalled, EnabledDisabled, + ProfileOrNot, } private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress || @@ -505,6 +506,7 @@ internal class PluginInstallerWindow : Window, IDisposable (Locs.SortBy_NewOrNot, PluginSortKind.NewOrNot), (Locs.SortBy_NotInstalled, PluginSortKind.NotInstalled), (Locs.SortBy_EnabledDisabled, PluginSortKind.EnabledDisabled), + (Locs.SortBy_ProfileOrNot, PluginSortKind.ProfileOrNot), }; var longestSelectableWidth = sortSelectables.Select(t => ImGui.CalcTextSize(t.Localization).X).Max(); var selectableWidth = longestSelectableWidth + (style.FramePadding.X * 2); // This does not include the label @@ -2984,6 +2986,12 @@ internal class PluginInstallerWindow : Window, IDisposable }); this.pluginListInstalled.Sort((p1, p2) => (p2.State == PluginState.Loaded).CompareTo(p1.State == PluginState.Loaded)); break; + case PluginSortKind.ProfileOrNot: + this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name)); + + var profman = Service.Get(); + this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.InternalName).CompareTo(profman.IsInDefaultProfile(p2.InternalName))); + break; default: throw new InvalidEnumArgumentException("Unknown plugin sort type."); } @@ -3062,6 +3070,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string SortBy_EnabledDisabled => Loc.Localize("InstallerEnabledDisabled", "Enabled/Disabled"); + public static string SortBy_ProfileOrNot => Loc.Localize("InstallerProfileOrNot", "In a collection"); + public static string SortBy_Label => Loc.Localize("InstallerSortBy", "Sort By"); #endregion From 9b920c8ddcb7e4f97438bad69092e46669044fac Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 18 Jul 2023 20:30:45 +0200 Subject: [PATCH 73/81] fix: profile tutorial text wrap --- .../PluginInstaller/ProfileManagerWidget.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 074ccdf32..301e43473 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -95,11 +95,20 @@ internal class ProfileManagerWidget ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphFour); ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommands); - ImGui.BulletText(Locs.TutorialCommandsEnable); - ImGui.BulletText(Locs.TutorialCommandsDisable); - ImGui.BulletText(Locs.TutorialCommandsToggle); - ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnd); + ImGui.Bullet(); + ImGui.SameLine(); + ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnable); + + ImGui.Bullet(); + ImGui.SameLine(); + ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsDisable); + + ImGui.Bullet(); + ImGui.SameLine(); + ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsToggle); + + ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnd); ImGuiHelpers.ScaledDummy(5); var buttonWidth = 120f; From f88b9435b1f986c87e5d78da76a79ed1dcf478dd Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 18 Jul 2023 20:34:15 +0200 Subject: [PATCH 74/81] build: 7.9.0.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index fbc9c1149..12068a35c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.8.0.0 + 7.9.0.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From b1dfaae579002f6f3716f453edab0085b0a9d49d Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 22 Jul 2023 14:40:02 +0200 Subject: [PATCH 75/81] Update ClientStructs (#1317) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 07bae81be..52a04305d 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 07bae81bede9923ec021b85f6119be394b93a757 +Subproject commit 52a04305d3f7321212083b5ccf1b83c136f1c09c From 26c09326126bdbbd56751bc53e2e3859d03fb068 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 26 Jul 2023 22:59:16 +0200 Subject: [PATCH 76/81] [master] Update ClientStructs (#1318) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 52a04305d..a6f5d730c 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 52a04305d3f7321212083b5ccf1b83c136f1c09c +Subproject commit a6f5d730c2fbf4a0521b0512fe41f4622d515218 From 26152f84228af94037a7e40914bdb279cc83fd26 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 26 Jul 2023 22:59:34 +0200 Subject: [PATCH 77/81] Disable window sounds for TitleScreenMenuWindow (#1316) --- Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 143fda6ab..10180f0c3 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -45,7 +45,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) { this.IsOpen = true; - + this.DisableWindowSounds = true; this.ForceMainWindow = true; this.Position = new Vector2(0, 200); From c37d8a15fdac1a3c4f86ac2cd5b9ca6de9c314aa Mon Sep 17 00:00:00 2001 From: Cara Date: Thu, 27 Jul 2023 06:30:22 +0930 Subject: [PATCH 78/81] Use own ConfigType enum (#1319) --- Dalamud/Game/Config/ConfigType.cs | 32 +++++++++++++++++++++++ Dalamud/Game/Config/SystemConfigOption.cs | 4 +-- Dalamud/Game/Config/UiConfigOption.cs | 4 +-- Dalamud/Game/Config/UiControlOption.cs | 4 +-- 4 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 Dalamud/Game/Config/ConfigType.cs diff --git a/Dalamud/Game/Config/ConfigType.cs b/Dalamud/Game/Config/ConfigType.cs new file mode 100644 index 000000000..731b26cc8 --- /dev/null +++ b/Dalamud/Game/Config/ConfigType.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Game.Config; + +/// +/// Types of options used by the game config. +/// +public enum ConfigType +{ + /// + /// Unused config index. + /// + Unused = 0, + + /// + /// A label entry with no value. + /// + Category = 1, + + /// + /// A config entry with an unsigned integer value. + /// + UInt = 2, + + /// + /// A config entry with a float value. + /// + Float = 3, + + /// + /// A config entry with a string value. + /// + String = 4, +} diff --git a/Dalamud/Game/Config/SystemConfigOption.cs b/Dalamud/Game/Config/SystemConfigOption.cs index 40d6b2fc9..70982311a 100644 --- a/Dalamud/Game/Config/SystemConfigOption.cs +++ b/Dalamud/Game/Config/SystemConfigOption.cs @@ -1,6 +1,4 @@ -using FFXIVClientStructs.FFXIV.Common.Configuration; - -namespace Dalamud.Game.Config; +namespace Dalamud.Game.Config; // ReSharper disable InconsistentNaming // ReSharper disable IdentifierTypo diff --git a/Dalamud/Game/Config/UiConfigOption.cs b/Dalamud/Game/Config/UiConfigOption.cs index 98fb4502d..82f823ffe 100644 --- a/Dalamud/Game/Config/UiConfigOption.cs +++ b/Dalamud/Game/Config/UiConfigOption.cs @@ -1,6 +1,4 @@ -using FFXIVClientStructs.FFXIV.Common.Configuration; - -namespace Dalamud.Game.Config; +namespace Dalamud.Game.Config; // ReSharper disable InconsistentNaming // ReSharper disable IdentifierTypo diff --git a/Dalamud/Game/Config/UiControlOption.cs b/Dalamud/Game/Config/UiControlOption.cs index 742df6b9f..5d36ee84d 100644 --- a/Dalamud/Game/Config/UiControlOption.cs +++ b/Dalamud/Game/Config/UiControlOption.cs @@ -1,6 +1,4 @@ -using FFXIVClientStructs.FFXIV.Common.Configuration; - -namespace Dalamud.Game.Config; +namespace Dalamud.Game.Config; // ReSharper disable InconsistentNaming // ReSharper disable IdentifierTypo From ead207fc67deeb0e5380b902bb2684481cca2b04 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 26 Jul 2023 14:15:14 -0700 Subject: [PATCH 79/81] Improve Search (#1305) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../PluginInstaller/PluginInstallerWindow.cs | 19 +- Dalamud/Utility/FuzzyMatcher.cs | 273 ++++++++++++++++++ 2 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 Dalamud/Utility/FuzzyMatcher.cs diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index f87e46855..a3787aaab 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -536,7 +536,8 @@ internal class PluginInstallerWindow : Window, IDisposable "###XlPluginInstaller_Search", Locs.Header_SearchPlaceholder, ref this.searchText, - 100); + 100, + ImGuiInputTextFlags.AutoSelectAll); ImGui.SameLine(); ImGui.SetCursorPosY(downShift); @@ -981,7 +982,7 @@ internal class PluginInstallerWindow : Window, IDisposable changelogs = this.dalamudChangelogManager.Changelogs.OfType(); } - var sortedChangelogs = changelogs?.Where(x => this.searchText.IsNullOrWhitespace() || x.Title.ToLowerInvariant().Contains(this.searchText.ToLowerInvariant())) + var sortedChangelogs = changelogs?.Where(x => this.searchText.IsNullOrWhitespace() || new FuzzyMatcher(this.searchText.ToLowerInvariant(), MatchMode.FuzzyParts).Matches(x.Title.ToLowerInvariant()) > 0) .OrderByDescending(x => x.Date).ToList(); if (sortedChangelogs == null || !sortedChangelogs.Any()) @@ -2889,8 +2890,8 @@ internal class PluginInstallerWindow : Window, IDisposable private bool IsManifestFiltered(IPluginManifest manifest) { - var searchString = this.searchText.ToLowerInvariant(); - var hasSearchString = !string.IsNullOrWhiteSpace(searchString); + var matcher = new FuzzyMatcher(this.searchText.ToLowerInvariant(), MatchMode.FuzzyParts); + var hasSearchString = !string.IsNullOrWhiteSpace(this.searchText); var oldApi = manifest.DalamudApiLevel < PluginManager.DalamudApiLevel; var installed = this.IsManifestInstalled(manifest).IsInstalled; @@ -2898,11 +2899,11 @@ internal class PluginInstallerWindow : Window, IDisposable return true; return hasSearchString && !( - (!manifest.Name.IsNullOrEmpty() && manifest.Name.ToLowerInvariant().Contains(searchString)) || - (!manifest.InternalName.IsNullOrEmpty() && manifest.InternalName.ToLowerInvariant().Contains(searchString)) || - (!manifest.Author.IsNullOrEmpty() && manifest.Author.Equals(this.searchText, StringComparison.InvariantCultureIgnoreCase)) || - (!manifest.Punchline.IsNullOrEmpty() && manifest.Punchline.ToLowerInvariant().Contains(searchString)) || - (manifest.Tags != null && manifest.Tags.Any(tag => tag.ToLowerInvariant().Contains(searchString)))); + (!manifest.Name.IsNullOrEmpty() && matcher.Matches(manifest.Name.ToLowerInvariant()) > 0) || + (!manifest.InternalName.IsNullOrEmpty() && matcher.Matches(manifest.InternalName.ToLowerInvariant()) > 0) || + (!manifest.Author.IsNullOrEmpty() && matcher.Matches(manifest.Author.ToLowerInvariant()) > 0) || + // (!manifest.Punchline.IsNullOrEmpty() && matcher.Matches(manifest.Punchline.ToLowerInvariant()) > 0) || // Removed because fuzzy match gets a little too excited with lots of random words + (manifest.Tags != null && matcher.MatchesAny(manifest.Tags.Select(term => term.ToLowerInvariant()).ToArray()) > 0)); } private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(IPluginManifest? manifest) diff --git a/Dalamud/Utility/FuzzyMatcher.cs b/Dalamud/Utility/FuzzyMatcher.cs new file mode 100644 index 000000000..647c9586d --- /dev/null +++ b/Dalamud/Utility/FuzzyMatcher.cs @@ -0,0 +1,273 @@ +#define BORDER_MATCHING + +namespace Dalamud.Utility; + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +internal readonly ref struct FuzzyMatcher +{ + private static readonly (int, int)[] EmptySegArray = Array.Empty<(int, int)>(); + + private readonly string needleString = string.Empty; + private readonly ReadOnlySpan needleSpan = ReadOnlySpan.Empty; + private readonly int needleFinalPosition = -1; + private readonly (int start, int end)[] needleSegments = EmptySegArray; + private readonly MatchMode mode = MatchMode.Simple; + + public FuzzyMatcher(string term, MatchMode matchMode) + { + needleString = term; + needleSpan = needleString.AsSpan(); + needleFinalPosition = needleSpan.Length - 1; + mode = matchMode; + + switch (matchMode) + { + case MatchMode.FuzzyParts: + needleSegments = FindNeedleSegments(needleSpan); + break; + case MatchMode.Fuzzy: + case MatchMode.Simple: + needleSegments = EmptySegArray; + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, null); + } + } + + private static (int start, int end)[] FindNeedleSegments(ReadOnlySpan span) + { + var segments = new List<(int, int)>(); + var wordStart = -1; + + for (var i = 0; i < span.Length; i++) + { + if (span[i] is not ' ' and not '\u3000') + { + if (wordStart < 0) + { + wordStart = i; + } + } + else if (wordStart >= 0) + { + segments.Add((wordStart, i - 1)); + wordStart = -1; + } + } + + if (wordStart >= 0) + { + segments.Add((wordStart, span.Length - 1)); + } + + return segments.ToArray(); + } + + public int Matches(string value) + { + if (needleFinalPosition < 0) + { + return 0; + } + + if (mode == MatchMode.Simple) + { + return value.Contains(needleString) ? 1 : 0; + } + + var haystack = value.AsSpan(); + + if (mode == MatchMode.Fuzzy) + { + return GetRawScore(haystack, 0, needleFinalPosition); + } + + if (mode == MatchMode.FuzzyParts) + { + if (needleSegments.Length < 2) + { + return GetRawScore(haystack, 0, needleFinalPosition); + } + + var total = 0; + for (var i = 0; i < needleSegments.Length; i++) + { + var (start, end) = needleSegments[i]; + var cur = GetRawScore(haystack, start, end); + if (cur == 0) + { + return 0; + } + + total += cur; + } + + return total; + } + + return 8; + } + + public int MatchesAny(params string[] values) + { + var max = 0; + for (var i = 0; i < values.Length; i++) + { + var cur = Matches(values[i]); + if (cur > max) + { + max = cur; + } + } + + return max; + } + + private int GetRawScore(ReadOnlySpan haystack, int needleStart, int needleEnd) + { + var (startPos, gaps, consecutive, borderMatches, endPos) = FindForward(haystack, needleStart, needleEnd); + if (startPos < 0) + { + return 0; + } + + var needleSize = needleEnd - needleStart + 1; + + var score = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches); + // PluginLog.Debug( + // $"['{needleString.Substring(needleStart, needleEnd - needleStart + 1)}' in '{haystack}'] fwd: needleSize={needleSize} startPos={startPos} gaps={gaps} consecutive={consecutive} borderMatches={borderMatches} score={score}"); + + (startPos, gaps, consecutive, borderMatches) = FindReverse(haystack, endPos, needleStart, needleEnd); + var revScore = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches); + // PluginLog.Debug( + // $"['{needleString.Substring(needleStart, needleEnd - needleStart + 1)}' in '{haystack}'] rev: needleSize={needleSize} startPos={startPos} gaps={gaps} consecutive={consecutive} borderMatches={borderMatches} score={revScore}"); + + return int.Max(score, revScore); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CalculateRawScore(int needleSize, int startPos, int gaps, int consecutive, int borderMatches) + { + var score = 100 + + needleSize * 3 + + borderMatches * 3 + + consecutive * 5 + - startPos + - gaps * 2; + if (startPos == 0) + score += 5; + return score < 1 ? 1 : score; + } + + private (int startPos, int gaps, int consecutive, int borderMatches, int haystackIndex) FindForward( + ReadOnlySpan haystack, int needleStart, int needleEnd) + { + var needleIndex = needleStart; + var lastMatchIndex = -10; + + var startPos = 0; + var gaps = 0; + var consecutive = 0; + var borderMatches = 0; + + for (var haystackIndex = 0; haystackIndex < haystack.Length; haystackIndex++) + { + if (haystack[haystackIndex] == needleSpan[needleIndex]) + { +#if BORDER_MATCHING + if (haystackIndex > 0) + { + if (!char.IsLetterOrDigit(haystack[haystackIndex - 1])) + { + borderMatches++; + } + } +#endif + + needleIndex++; + + if (haystackIndex == lastMatchIndex + 1) + { + consecutive++; + } + + if (needleIndex > needleEnd) + { + return (startPos, gaps, consecutive, borderMatches, haystackIndex); + } + + lastMatchIndex = haystackIndex; + } + else + { + if (needleIndex > needleStart) + { + gaps++; + } + else + { + startPos++; + } + } + } + + return (-1, 0, 0, 0, 0); + } + + private (int startPos, int gaps, int consecutive, int borderMatches) FindReverse(ReadOnlySpan haystack, + int haystackLastMatchIndex, int needleStart, int needleEnd) + { + var needleIndex = needleEnd; + var revLastMatchIndex = haystack.Length + 10; + + var gaps = 0; + var consecutive = 0; + var borderMatches = 0; + + for (var haystackIndex = haystackLastMatchIndex; haystackIndex >= 0; haystackIndex--) + { + if (haystack[haystackIndex] == needleSpan[needleIndex]) + { +#if BORDER_MATCHING + if (haystackIndex > 0) + { + if (!char.IsLetterOrDigit(haystack[haystackIndex - 1])) + { + borderMatches++; + } + } +#endif + + needleIndex--; + + if (haystackIndex == revLastMatchIndex - 1) + { + consecutive++; + } + + if (needleIndex < needleStart) + { + return (haystackIndex, gaps, consecutive, borderMatches); + } + + revLastMatchIndex = haystackIndex; + } + else + { + gaps++; + } + } + + return (-1, 0, 0, 0); + } +} + +public enum MatchMode +{ + Simple, + Fuzzy, + FuzzyParts +} From d4c68a480fae9c39ff2ec5833423b11af72a6861 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 27 Jul 2023 00:26:29 +0200 Subject: [PATCH 80/81] ci: fix rollup fetch depth (#1320) --- .github/workflows/rollup.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml index 2bf754100..25b558711 100644 --- a/.github/workflows/rollup.yml +++ b/.github/workflows/rollup.yml @@ -22,6 +22,7 @@ jobs: uses: actions/checkout@v3 with: submodules: true + fetch-depth: 0 ref: ${{ matrix.branches }} token: ${{ secrets.UPDATE_PAT }} - name: Create update branch From 8d49d285522d8e2a6595fe28fd6f51893032b4c0 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 27 Jul 2023 14:02:34 +0200 Subject: [PATCH 81/81] Add ImRaii.ContextPopupItem (#1322) --- Dalamud.Interface/Raii/EndObjects.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dalamud.Interface/Raii/EndObjects.cs b/Dalamud.Interface/Raii/EndObjects.cs index 2e5ad30d0..032f09621 100644 --- a/Dalamud.Interface/Raii/EndObjects.cs +++ b/Dalamud.Interface/Raii/EndObjects.cs @@ -51,6 +51,12 @@ public static partial class ImRaii public static IEndObject ContextPopup(string id, ImGuiPopupFlags flags) => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextWindow(id, flags)); + public static IEndObject ContextPopupItem(string id) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextItem(id)); + + public static IEndObject ContextPopupItem(string id, ImGuiPopupFlags flags) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextItem(id, flags)); + public static IEndObject Combo(string label, string previewValue) => new EndConditionally(ImGui.EndCombo, ImGui.BeginCombo(label, previewValue));