From ebc4baca585abadcd326292d379a7ba0408537a1 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 21:36:46 +0200 Subject: [PATCH 01/40] fix: return DalamudTextureWrap from LoadImageFromDxgiFormat(), cast explicitly --- Dalamud/Interface/Internal/InterfaceManager.cs | 4 ++-- Dalamud/Interface/Internal/TextureManager.cs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 841511f55..f46c7272d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -325,7 +325,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// The height in pixels. /// Format of the texture. /// A texture, ready to use in ImGui. - public TextureWrap LoadImageFromDxgiFormat(Span data, int pitch, int width, int height, Format dxgiFormat) + public IDalamudTextureWrap LoadImageFromDxgiFormat(Span data, int pitch, int width, int height, Format dxgiFormat) { if (this.scene == null) throw new InvalidOperationException("Scene isn't ready."); @@ -360,7 +360,7 @@ internal class InterfaceManager : IDisposable, IServiceType } // no sampler for now because the ImGui implementation we copied doesn't allow for changing it - return new D3DTextureWrap(resView, width, height); + return new DalamudTextureWrap(new D3DTextureWrap(resView, width, height)); } #nullable restore diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 4b2f1f362..a7c5a005e 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -209,7 +209,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP throw new InvalidOperationException("Cannot create textures before scene is ready"); #pragma warning disable CS0618 - return this.dataManager.GetImGuiTexture(file) as IDalamudTextureWrap; + return (IDalamudTextureWrap)this.dataManager.GetImGuiTexture(file); #pragma warning restore CS0618 } @@ -332,6 +332,10 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP // and we can't do anything about it. Return a dummy texture so that the plugin still // has something to draw. wrap = this.fallbackTextureWrap; + + // Prevent divide-by-zero + if (info.Extents == Vector2.Zero) + info.Extents = Vector2.One; } info.Wrap = wrap; From dc95d7e8de55b5c1bbcca915c83385c757118a33 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 21:39:01 +0200 Subject: [PATCH 02/40] build: 7.10.1.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 494c0f297..e2da1a057 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.10.0.0 + 7.10.1.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 39e389080ce515ecc85511f40beeeec247de6522 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 22:04:30 +0200 Subject: [PATCH 03/40] chore: throw if TextureManagerTextureWrap is used beyond disposal --- Dalamud/Interface/Internal/TextureManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index a7c5a005e..717bd8081 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -574,7 +574,9 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap } /// - public IntPtr ImGuiHandle => this.manager.GetInfo(this.path).Wrap!.ImGuiHandle; + public IntPtr ImGuiHandle => !this.IsDisposed ? + this.manager.GetInfo(this.path).Wrap!.ImGuiHandle : + throw new InvalidOperationException("Texture already disposed. You may not render it."); /// public int Width { get; private set; } From d1fad810cfe74ca320bdd2ad3902fe04bc5ed95c Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 9 Aug 2023 01:42:05 +0200 Subject: [PATCH 04/40] fix: set initial refcount inside lock to prevent race condition with cleanup code when loading from a task --- Dalamud/Interface/Internal/TextureManager.cs | 46 +++++++++++++------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 717bd8081..294edbb0a 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -239,24 +239,33 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// If false, exceptions will be caught and a dummy texture will be returned to prevent plugins from using invalid texture handles. /// /// Info object storing texture metadata. - internal TextureInfo GetInfo(string path, bool refresh = true, bool rethrow = false) + internal TextureInfo? GetInfo(string path, bool refresh = true, bool rethrow = false) { TextureInfo? info; lock (this.activeTextures) { - this.activeTextures.TryGetValue(path, out info); + if (!this.activeTextures.TryGetValue(path, out info)) + { + Debug.Assert(rethrow, "This should never run when getting outside of creator"); + + if (!refresh) + return null; + + // NOTE: We need to init the refcount here while locking the collection! + // Otherwise, if this is loaded from a task, cleanup might already try to delete it + // before it can be increased. + info = new TextureInfo + { + RefCount = 1, + }; + + this.activeTextures.Add(path, info); + } + + if (info == null) + throw new Exception("null info in activeTextures"); } - if (info == null) - { - info = new TextureInfo(); - lock (this.activeTextures) - { - if (!this.activeTextures.TryAdd(path, info)) - Log.Warning("Texture {Path} tracked twice", path); - } - } - if (refresh && info.KeepAliveCount == 0) info.LastAccess = DateTime.UtcNow; @@ -353,6 +362,14 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP internal void NotifyTextureDisposed(string path, bool keepAlive) { var info = this.GetInfo(path, false); + + // This texture was already disposed + if (info == null) + { + Log.Warning("Disposing unknown texture {Path}", path); + return; + } + info.RefCount--; if (keepAlive) @@ -378,8 +395,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP { // This will create the texture. // That's fine, it's probably used immediately and this will let the plugin catch load errors. - var info = this.GetInfo(path, rethrow: true); - info.RefCount++; + var info = this.GetInfo(path, rethrow: true)!; if (keepAlive) info.KeepAliveCount++; @@ -575,7 +591,7 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap /// public IntPtr ImGuiHandle => !this.IsDisposed ? - this.manager.GetInfo(this.path).Wrap!.ImGuiHandle : + this.manager.GetInfo(this.path)!.Wrap!.ImGuiHandle : throw new InvalidOperationException("Texture already disposed. You may not render it."); /// From 5cfddb23dd1b79f6470cb0fdb5b0437dc0ed25e5 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 9 Aug 2023 01:46:52 +0200 Subject: [PATCH 05/40] fix: actually increase refcount if we have multiple handles --- Dalamud/Interface/Internal/TextureManager.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 294edbb0a..66a2cf731 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -250,20 +250,18 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP if (!refresh) return null; - - // NOTE: We need to init the refcount here while locking the collection! - // Otherwise, if this is loaded from a task, cleanup might already try to delete it - // before it can be increased. - info = new TextureInfo - { - RefCount = 1, - }; + info = new TextureInfo(); this.activeTextures.Add(path, info); } if (info == null) throw new Exception("null info in activeTextures"); + + // NOTE: We need to increase the refcount here while locking the collection! + // Otherwise, if this is loaded from a task, cleanup might already try to delete it + // before it can be increased. + info.RefCount++; } if (refresh && info.KeepAliveCount == 0) From 8a300cc98e96ca569c4f969c1ce734a5c8287f91 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 9 Aug 2023 20:19:15 +0200 Subject: [PATCH 06/40] Update ClientStructs (#1344) 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 155511a61..9b2eab0f2 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 155511a61559c529719d3810eaf8fb9336482878 +Subproject commit 9b2eab0f212030c062427b307b96118881d36b99 From 24ad2d4c8b4ab5ba4effd03253752d58dea0e124 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 9 Aug 2023 21:22:29 +0200 Subject: [PATCH 07/40] chore: simplify refcounting logic, more concurrency fixes --- Dalamud/Interface/Internal/TextureManager.cs | 185 +++++++++---------- 1 file changed, 90 insertions(+), 95 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 66a2cf731..905cac55a 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -233,13 +233,12 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// Get texture info. /// /// Path to the texture. - /// Whether or not the texture should be reloaded if it was unloaded. /// /// If true, exceptions caused by texture load will not be caught. /// If false, exceptions will be caught and a dummy texture will be returned to prevent plugins from using invalid texture handles. /// /// Info object storing texture metadata. - internal TextureInfo? GetInfo(string path, bool refresh = true, bool rethrow = false) + internal TextureInfo GetInfo(string path, bool rethrow = false) { TextureInfo? info; lock (this.activeTextures) @@ -248,106 +247,94 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP { Debug.Assert(rethrow, "This should never run when getting outside of creator"); - if (!refresh) - return null; - info = new TextureInfo(); this.activeTextures.Add(path, info); } if (info == null) throw new Exception("null info in activeTextures"); - - // NOTE: We need to increase the refcount here while locking the collection! - // Otherwise, if this is loaded from a task, cleanup might already try to delete it - // before it can be increased. - info.RefCount++; } - if (refresh && info.KeepAliveCount == 0) + if (info.KeepAliveCount == 0) info.LastAccess = DateTime.UtcNow; if (info is { Wrap: not null }) return info; - if (refresh) - { - if (!this.im.IsReady) + if (!this.im.IsReady) throw new InvalidOperationException("Cannot create textures before scene is ready"); - string? interceptPath = null; - this.InterceptTexDataLoad?.Invoke(path, ref interceptPath); + string? interceptPath = null; + this.InterceptTexDataLoad?.Invoke(path, ref interceptPath); - if (interceptPath != null) - { - Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath); - path = interceptPath; - } + if (interceptPath != null) + { + Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath); + path = interceptPath; + } - TextureWrap? wrap; - try + TextureWrap? wrap; + try + { + // We want to load this from the disk, probably, if the path has a root + // Not sure if this can cause issues with e.g. network drives, might have to rethink + // and add a flag instead if it does. + if (Path.IsPathRooted(path)) { - // We want to load this from the disk, probably, if the path has a root - // Not sure if this can cause issues with e.g. network drives, might have to rethink - // and add a flag instead if it does. - if (Path.IsPathRooted(path)) + if (Path.GetExtension(path) == ".tex") { - if (Path.GetExtension(path) == ".tex") - { - // Attempt to load via Lumina - var file = this.dataManager.GameData.GetFileFromDisk(path); - wrap = this.GetTexture(file); - Log.Verbose("Texture {Path} loaded FS via Lumina", path); - } - else - { - // Attempt to load image - wrap = this.im.LoadImage(path); - Log.Verbose("Texture {Path} loaded FS via LoadImage", path); - } + // Attempt to load via Lumina + var file = this.dataManager.GameData.GetFileFromDisk(path); + wrap = this.GetTexture(file); + Log.Verbose("Texture {Path} loaded FS via Lumina", path); } else { - // Load regularly from dats - var file = this.dataManager.GetFile(path); - if (file == null) - throw new Exception("Could not load TexFile from dat."); - - wrap = this.GetTexture(file); - Log.Verbose("Texture {Path} loaded from SqPack", path); + // Attempt to load image + wrap = this.im.LoadImage(path); + Log.Verbose("Texture {Path} loaded FS via LoadImage", path); } - - if (wrap == null) - throw new Exception("Could not create texture"); - - // TODO: We could support this, but I don't think it's worth it at the moment. - var extents = new Vector2(wrap.Width, wrap.Height); - if (info.Extents != Vector2.Zero && info.Extents != extents) - Log.Warning("Texture at {Path} changed size between reloads, this is currently not supported.", path); - - info.Extents = extents; } - catch (Exception e) + else { - Log.Error(e, "Could not load texture from {Path}", path); - - // When creating the texture initially, we want to be able to pass errors back to the plugin - if (rethrow) - throw; - - // This means that the load failed due to circumstances outside of our control, - // and we can't do anything about it. Return a dummy texture so that the plugin still - // has something to draw. - wrap = this.fallbackTextureWrap; - - // Prevent divide-by-zero - if (info.Extents == Vector2.Zero) - info.Extents = Vector2.One; + // Load regularly from dats + var file = this.dataManager.GetFile(path); + if (file == null) + throw new Exception("Could not load TexFile from dat."); + + wrap = this.GetTexture(file); + Log.Verbose("Texture {Path} loaded from SqPack", path); } + + if (wrap == null) + throw new Exception("Could not create texture"); - info.Wrap = wrap; + // TODO: We could support this, but I don't think it's worth it at the moment. + var extents = new Vector2(wrap.Width, wrap.Height); + if (info.Extents != Vector2.Zero && info.Extents != extents) + Log.Warning("Texture at {Path} changed size between reloads, this is currently not supported.", path); + + info.Extents = extents; + } + catch (Exception e) + { + Log.Error(e, "Could not load texture from {Path}", path); + + // When creating the texture initially, we want to be able to pass errors back to the plugin + if (rethrow) + throw; + + // This means that the load failed due to circumstances outside of our control, + // and we can't do anything about it. Return a dummy texture so that the plugin still + // has something to draw. + wrap = this.fallbackTextureWrap; + + // Prevent divide-by-zero + if (info.Extents == Vector2.Zero) + info.Extents = Vector2.One; } + info.Wrap = wrap; return info; } @@ -359,23 +346,23 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// Whether or not this handle was created in keep-alive mode. internal void NotifyTextureDisposed(string path, bool keepAlive) { - var info = this.GetInfo(path, false); - - // This texture was already disposed - if (info == null) + lock (this.activeTextures) { - Log.Warning("Disposing unknown texture {Path}", path); - return; + if (!this.activeTextures.TryGetValue(path, out var info)) + { + Log.Warning("Disposing texture that didn't exist: {Path}", path); + return; + } + + info.RefCount--; + + if (keepAlive) + info.KeepAliveCount--; + + // Clean it up by the next update. If it's re-requested in-between, we don't reload it. + if (info.RefCount <= 0) + info.LastAccess = default; } - - info.RefCount--; - - if (keepAlive) - info.KeepAliveCount--; - - // Clean it up by the next update. If it's re-requested in-between, we don't reload it. - if (info.RefCount <= 0) - info.LastAccess = default; } private static string FormatIconPath(uint iconId, string? type, bool highResolution) @@ -391,14 +378,22 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP private TextureManagerTextureWrap? CreateWrap(string path, bool keepAlive) { - // This will create the texture. - // That's fine, it's probably used immediately and this will let the plugin catch load errors. - var info = this.GetInfo(path, rethrow: true)!; + lock (this.activeTextures) + { + // This will create the texture. + // That's fine, it's probably used immediately and this will let the plugin catch load errors. + var info = this.GetInfo(path, rethrow: true); - if (keepAlive) - info.KeepAliveCount++; + // We need to increase the refcounts here while locking the collection! + // Otherwise, if this is loaded from a task, cleanup might already try to delete it + // before it can be increased. + info.RefCount++; - return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this); + if (keepAlive) + info.KeepAliveCount++; + + return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this); + } } private void FrameworkOnUpdate(Framework fw) @@ -589,7 +584,7 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap /// public IntPtr ImGuiHandle => !this.IsDisposed ? - this.manager.GetInfo(this.path)!.Wrap!.ImGuiHandle : + this.manager.GetInfo(this.path).Wrap!.ImGuiHandle : throw new InvalidOperationException("Texture already disposed. You may not render it."); /// From 4c15df80b9cf636d7e08c106292818e704d495f0 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 10 Aug 2023 19:11:48 +0200 Subject: [PATCH 08/40] feat: add ITextureSubstitutionProvider.InvalidatePaths() --- Dalamud/Interface/Internal/TextureManager.cs | 45 +++++++++++++++---- .../Services/ITextureSubstitutionProvider.cs | 19 +++++++- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 905cac55a..ad7c0f2f8 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -213,6 +213,39 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP #pragma warning restore CS0618 } + /// + public string GetSubstitutedPath(string originalPath) + { + if (this.InterceptTexDataLoad == null) + return originalPath; + + string? interceptPath = null; + this.InterceptTexDataLoad.Invoke(originalPath, ref interceptPath); + + if (interceptPath != null) + { + Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", originalPath, interceptPath); + return interceptPath; + } + + return originalPath; + } + + /// + public void InvalidatePaths(IEnumerable paths) + { + lock (this.activeTextures) + { + foreach (var path in paths) + { + if (!this.activeTextures.TryGetValue(path, out var info) || info == null) + continue; + + info.Wrap = null; + } + } + } + /// public void Dispose() { @@ -263,16 +296,10 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP if (!this.im.IsReady) throw new InvalidOperationException("Cannot create textures before scene is ready"); - - string? interceptPath = null; - this.InterceptTexDataLoad?.Invoke(path, ref interceptPath); - - if (interceptPath != null) - { - Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath); - path = interceptPath; - } + // Substitute the path here for loading, instead of when getting the respective TextureInfo + path = this.GetSubstitutedPath(path); + TextureWrap? wrap; try { diff --git a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs index 90be71adb..3ddd7d13e 100644 --- a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs +++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Plugin.Services; +using System.Collections.Generic; + +namespace Dalamud.Plugin.Services; /// /// Service that grants you the ability to replace texture data that is to be loaded by Dalamud. @@ -17,4 +19,19 @@ public interface ITextureSubstitutionProvider /// Event that will be called once Dalamud wants to load texture data. /// public event TextureDataInterceptorDelegate? InterceptTexDataLoad; + + /// + /// Get a path that may be substituted by a subscriber to ITextureSubstitutionProvider. + /// + /// The original path to substitute. + /// The original path, if no subscriber is registered or there is no substitution, or the substituted path. + public string GetSubstitutedPath(string originalPath); + + /// + /// Notify Dalamud about substitution status for files at the specified VFS paths changing. + /// You should call this with all paths that were either previously substituted and are no longer, + /// and paths that are newly substituted. + /// + /// The paths with a changed substitution status. + public void InvalidatePaths(IEnumerable paths); } From bfbfe8c91867132297e00b45021d33ae54dc2fe2 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 12 Aug 2023 12:03:45 +0200 Subject: [PATCH 09/40] chore: ModuleLog.Error() exception can be nullable --- Dalamud/Logging/Internal/ModuleLog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index d93730f36..c6c66e81a 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -108,7 +108,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Error(Exception exception, string messageTemplate, params object[] values) + public void Error(Exception? exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values); /// From 593b338a26e72bf91aa93b37d43d2181b49db6f4 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 12 Aug 2023 12:04:22 +0200 Subject: [PATCH 10/40] fix: don't try to load changelogs for dev plugins, show changelogs that loaded if any failed --- Dalamud/Interface/Internal/TextureManager.cs | 1 + .../DalamudChangelogManager.cs | 29 ++++++++++++------- .../PluginInstaller/PluginInstallerWindow.cs | 9 +++++- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index ad7c0f2f8..1bc5198e3 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -241,6 +241,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP if (!this.activeTextures.TryGetValue(path, out var info) || info == null) continue; + info.Wrap?.Dispose(); info.Wrap = null; } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs index 984732509..a9ad0c21a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Utility; +using Serilog; namespace Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -44,27 +45,35 @@ internal class DalamudChangelogManager this.Changelogs = null; var dalamudChangelogs = await client.GetFromJsonAsync>(DalamudChangelogUrl); - var changelogs = dalamudChangelogs.Select(x => new DalamudChangelogEntry(x)).Cast(); + var changelogs = dalamudChangelogs.Select(x => new DalamudChangelogEntry(x)).Cast().ToList(); foreach (var plugin in this.manager.InstalledPlugins) { - if (!plugin.IsThirdParty) + if (!plugin.IsThirdParty && !plugin.IsDev) { - var pluginChangelogs = await client.GetFromJsonAsync(string.Format( - PluginChangelogUrl, - plugin.Manifest.InternalName, - plugin.Manifest.Dip17Channel)); + try + { + var pluginChangelogs = await client.GetFromJsonAsync(string.Format( + PluginChangelogUrl, + plugin.Manifest.InternalName, + plugin.Manifest.Dip17Channel)); - changelogs = changelogs.Concat(pluginChangelogs.Versions - .Where(x => x.Dip17Track == plugin.Manifest.Dip17Channel) - .Select(x => new PluginChangelogEntry(plugin, x))); + changelogs.AddRange(pluginChangelogs.Versions + .Where(x => x.Dip17Track == + plugin.Manifest.Dip17Channel) + .Select(x => new PluginChangelogEntry(plugin, x))); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load changelog for {PluginName}", plugin.Manifest.Name); + } } else { if (plugin.Manifest.Changelog.IsNullOrWhitespace()) continue; - changelogs = changelogs.Append(new PluginChangelogEntry(plugin)); + changelogs.Add(new PluginChangelogEntry(plugin)); } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 35fa40013..4cc7e35c3 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -967,7 +967,14 @@ internal class PluginInstallerWindow : Window, IDisposable { this.dalamudChangelogRefreshTaskCts = new CancellationTokenSource(); this.dalamudChangelogRefreshTask = - Task.Run(this.dalamudChangelogManager.ReloadChangelogAsync, this.dalamudChangelogRefreshTaskCts.Token); + Task.Run(this.dalamudChangelogManager.ReloadChangelogAsync, this.dalamudChangelogRefreshTaskCts.Token) + .ContinueWith(t => + { + if (!t.IsCompletedSuccessfully) + { + Log.Error(t.Exception, "Failed to load changelogs."); + } + }); } return; From 7c428e6b72422aca79cbcfc3f60cab0f3d47c615 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 13 Aug 2023 22:42:59 +0200 Subject: [PATCH 11/40] feat: allow individual toggling of plugins in a single custom collection from the installed plugins page --- .../PluginInstaller/PluginInstallerWindow.cs | 109 ++++++++++-------- Dalamud/Plugin/Internal/Profiles/Profile.cs | 3 + 2 files changed, 63 insertions(+), 49 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 4cc7e35c3..b648a8204 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2366,6 +2366,10 @@ internal class PluginInstallerWindow : Window, IDisposable var config = Service.Get(); var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev; + var profilesThatWantThisPlugin = profileManager.Profiles + .Where(x => x.WantsPlugin(plugin.InternalName) != null) + .ToArray(); + var isInSingleProfile = profilesThatWantThisPlugin.Length == 1; var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName); // Disable everything if the updater is running or another plugin is operating @@ -2449,6 +2453,10 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.EndPopup(); } + var inMultipleProfiles = !isDefaultPlugin && !isInSingleProfile; + var inSingleNonDefaultProfileWhichIsDisabled = + isInSingleProfile && !profilesThatWantThisPlugin.First().IsEnabled; + if (plugin.State is PluginState.UnloadError or PluginState.LoadError or PluginState.DependencyResolutionFailed && !plugin.IsDev) { ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown); @@ -2456,80 +2464,77 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.PluginButtonToolTip_UnloadFailed); } - else if (disabled || !isDefaultPlugin) + else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled) { ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable); - if (!isDefaultPlugin && ImGui.IsItemHovered()) - ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInDefault); + if (inMultipleProfiles && ImGui.IsItemHovered()) + ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInSingleProfile); + else if (inSingleNonDefaultProfileWhichIsDisabled && ImGui.IsItemHovered()) + ImGui.SetTooltip(Locs.PluginButtonToolTip_SingleProfileDisabled(profilesThatWantThisPlugin.First().Name)); } else { if (ImGuiComponents.ToggleButton(toggleId, ref isLoadedAndUnloadable)) { - // TODO: We can technically let profile manager take care of unloading/loading the plugin, but we should figure out error handling first. + var applicableProfile = profilesThatWantThisPlugin.First(); + Log.Verbose("Switching {InternalName} in {Profile} to {State}", + plugin.InternalName, applicableProfile, isLoadedAndUnloadable); + + try + { + // Reload the devPlugin manifest if it's a dev plugin + // The plugin might rely on changed values in the manifest + if (plugin.IsDev) + { + plugin.ReloadManifest(); + } + } + catch (Exception ex) + { + Log.Error(ex, "Could not reload DevPlugin manifest"); + } + + // NOTE: We don't use the profile manager to actually handle loading/unloading here, + // because that might cause us to show an error if a plugin we don't actually care about + // fails to load/unload. Instead, we just do it ourselves and then update the profile. + // There is probably a smarter way to handle this, but it's probably more code. if (!isLoadedAndUnloadable) { this.enableDisableStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.DisablingSingle; - Task.Run(() => + Task.Run(async () => { - if (plugin.IsDev) - { - plugin.ReloadManifest(); - } - - var unloadTask = Task.Run(() => plugin.UnloadAsync()) - .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name)); - - unloadTask.Wait(); - if (!unloadTask.Result) - { - this.enableDisableStatus = OperationStatus.Complete; - return; - } - - // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, false, false)) - .GetAwaiter().GetResult(); - this.enableDisableStatus = OperationStatus.Complete; + await plugin.UnloadAsync(); + await applicableProfile.AddOrUpdateAsync( + plugin.Manifest.InternalName, false, false); notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); + }).ContinueWith(t => + { + this.enableDisableStatus = OperationStatus.Complete; + this.DisplayErrorContinuation(t, Locs.ErrorModal_UnloadFail(plugin.Name)); }); } else { - var enabler = new Task(() => + async Task Enabler() { this.enableDisableStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; - if (plugin.IsDev) - { - plugin.ReloadManifest(); - } + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false); + await plugin.LoadAsync(PluginLoadReason.Installer); - // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false)) - .GetAwaiter().GetResult(); + notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success); + } - var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer)) - .ContinueWith( - this.DisplayErrorContinuation, - Locs.ErrorModal_LoadFail(plugin.Name)); - - loadTask.Wait(); + var continuation = (Task t) => + { this.enableDisableStatus = OperationStatus.Complete; - - if (!loadTask.Result) - return; - - notifications.AddNotification( - Locs.Notifications_PluginEnabled(plugin.Manifest.Name), - Locs.Notifications_PluginEnabledTitle, - NotificationType.Success); - }); + this.DisplayErrorContinuation(t, Locs.ErrorModal_LoadFail(plugin.Name)); + }; if (availableUpdate != default && !availableUpdate.InstalledPlugin.IsDev) { @@ -2539,17 +2544,19 @@ internal class PluginInstallerWindow : Window, IDisposable if (shouldUpdate) { + // We need to update the profile right here, because PM will not enable the plugin otherwise + await applicableProfile.AddOrUpdateAsync(plugin.InternalName, true, false); await this.UpdateSinglePlugin(availableUpdate); } else { - enabler.Start(); + _ = Task.Run(Enabler).ContinueWith(continuation); } }); } else { - enabler.Start(); + _ = Task.Run(Enabler).ContinueWith(continuation); } } } @@ -3259,6 +3266,10 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButtonToolTip_UnloadFailed => Loc.Localize("InstallerLoadUnloadFailedTooltip", "Plugin load/unload failed, please restart your game and try again."); public static string PluginButtonToolTip_NeedsToBeInDefault => Loc.Localize("InstallerUnloadNeedsToBeInDefault", "This plugin is in one or more collections. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it manually, remove it from all collections."); + + public static string PluginButtonToolTip_NeedsToBeInSingleProfile => Loc.Localize("InstallerUnloadNeedsToBeInSingleProfile", "This plugin is in more than one collection. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it here, make sure it is only in a single collection."); + + public static string PluginButtonToolTip_SingleProfileDisabled(string name) => Loc.Localize("InstallerSingleProfileDisabled", "The collection '{0}' which contains this plugin is disabled.\nPlease enable it in the collections manager to toggle the plugin individually.").Format(name); #endregion diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 61d521e89..ac46d9153 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -232,4 +232,7 @@ internal class Profile if (apply) await this.manager.ApplyAllWantStatesAsync(); } + + /// + public override string ToString() => $"{this.Guid} ({this.Name})"; } From 78400c8cb6ea95b6cfbdbf362dbfdcc5e4649673 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 16 Aug 2023 02:35:30 +0200 Subject: [PATCH 12/40] Update ClientStructs (#1346) 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 9b2eab0f2..e6d2de724 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9b2eab0f212030c062427b307b96118881d36b99 +Subproject commit e6d2de7240f8cbc6a9a2a7133ad633ebfef6d33e From c0f828f5fa3e793ec9aa6fe0059f78f2b8753095 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 19 Aug 2023 20:18:37 +0200 Subject: [PATCH 13/40] Update ClientStructs (#1349) 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 e6d2de724..81833c4c8 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e6d2de7240f8cbc6a9a2a7133ad633ebfef6d33e +Subproject commit 81833c4c8b79bdcbb21fc6fbfb3d298767368872 From 8cbe7cbb3068bb4d04adc1eb2642aa1cb7376504 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 22 Aug 2023 02:51:40 +0200 Subject: [PATCH 14/40] Update ClientStructs (#1352) 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 81833c4c8..de4c356bf 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 81833c4c8b79bdcbb21fc6fbfb3d298767368872 +Subproject commit de4c356bf27da2e3d596478c0192ce0baa75c125 From a0edd21620f4e3c80616455aca2c6fdd68a30a64 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 22 Aug 2023 09:11:09 -0700 Subject: [PATCH 15/40] build: 7.10.2.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index e2da1a057..be04c5a20 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.10.1.0 + 7.10.2.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 07692753dbeccdbda100451db3117a52563523a6 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 22 Aug 2023 21:49:38 +0200 Subject: [PATCH 16/40] fix: don't draw changelog twice --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index b648a8204..6f7496c72 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2149,6 +2149,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}"); var hasChangelog = !plugin.Manifest.Changelog.IsNullOrEmpty(); + var didDrawChangelogInsideCollapsible = false; if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index)) { @@ -2257,6 +2258,7 @@ internal class PluginInstallerWindow : Window, IDisposable { if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion))) { + didDrawChangelogInsideCollapsible = true; this.DrawInstalledPluginChangelog(plugin.Manifest); ImGui.TreePop(); } @@ -2273,7 +2275,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - if (thisWasUpdated && hasChangelog) + if (thisWasUpdated && hasChangelog && !didDrawChangelogInsideCollapsible) { this.DrawInstalledPluginChangelog(plugin.Manifest); } From c027aacde2020846cfeec141e6426d731c397103 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 22 Aug 2023 22:19:10 +0200 Subject: [PATCH 17/40] feat: add ImGuiComponents.IconButtonWithText() --- .../Components/ImGuiComponents.IconButton.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs index 99e43d68c..05e660b61 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using ImGuiNET; @@ -119,4 +120,71 @@ public static partial class ImGuiComponents return button; } + + /// + /// IconButton component to use an icon as a button with color options. + /// + /// Icon to show. + /// Text to show. + /// The default color of the button. + /// The color of the button when active. + /// The color of the button when hovered. + /// Indicator if button is clicked. + public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null) + { + var numColors = 0; + + if (defaultColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value); + numColors++; + } + + if (activeColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value); + numColors++; + } + + if (hoveredColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value); + numColors++; + } + + ImGui.PushID(text); + + ImGui.PushFont(UiBuilder.IconFont); + var iconSize = ImGui.CalcTextSize(icon.ToIconString()); + ImGui.PopFont(); + + var textSize = ImGui.CalcTextSize(text); + var dl = ImGui.GetWindowDrawList(); + var cursor = ImGui.GetCursorScreenPos(); + + var iconPadding = 3 * ImGuiHelpers.GlobalScale; + + // Draw an ImGui button with the icon and text + var buttonWidth = iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding; + var buttonHeight = Math.Max(iconSize.Y, textSize.Y) + (ImGui.GetStyle().FramePadding.Y * 2); + var button = ImGui.Button(string.Empty, new Vector2(buttonWidth, buttonHeight)); + + // Draw the icon on the window drawlist + var iconPos = new Vector2(cursor.X + ImGui.GetStyle().FramePadding.X, cursor.Y + ImGui.GetStyle().FramePadding.Y); + + ImGui.PushFont(UiBuilder.IconFont); + dl.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); + ImGui.PopFont(); + + // Draw the text on the window drawlist + var textPos = new Vector2(iconPos.X + iconSize.X + iconPadding, cursor.Y + ImGui.GetStyle().FramePadding.Y); + dl.AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), text); + + ImGui.PopID(); + + if (numColors > 0) + ImGui.PopStyleColor(numColors); + + return button; + } } From 3272dbb0e2343242338cadb6b57bfc6549f8286c Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 22 Aug 2023 22:20:08 +0200 Subject: [PATCH 18/40] feat: add OpenMainUi event on UiBuilder, respective button in PI --- Dalamud.CorePlugin/PluginImpl.cs | 7 +++ .../PluginInstaller/PluginInstallerWindow.cs | 48 +++++++++++++++++-- Dalamud/Interface/UiBuilder.cs | 18 +++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 9026ea0dd..b858e9a0c 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; +using Serilog; namespace Dalamud.CorePlugin { @@ -66,6 +67,7 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; + this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." }); @@ -143,6 +145,11 @@ namespace Dalamud.CorePlugin // this.window.IsOpen = true; } + private void OnOpenMainUi() + { + Log.Verbose("Opened main UI"); + } + #endif } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 6f7496c72..ad9d77754 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2227,6 +2227,8 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGuiHelpers.SafeTextWrapped($"{command.Key} → {command.Value.HelpMessage}"); } + + ImGuiHelpers.ScaledDummy(3); } } @@ -2573,6 +2575,9 @@ internal class PluginInstallerWindow : Window, IDisposable { // Only if the plugin isn't broken. this.DrawOpenPluginSettingsButton(plugin); + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(5, 0); } if (applicableForProfiles && config.ProfilesEnabled) @@ -2637,10 +2642,39 @@ internal class PluginInstallerWindow : Window, IDisposable private void DrawOpenPluginSettingsButton(LocalPlugin plugin) { - if (plugin.DalamudInterface?.UiBuilder?.HasConfigUi ?? false) + var hasMainUi = plugin.DalamudInterface?.UiBuilder.HasMainUi ?? false; + var hasConfig = plugin.DalamudInterface?.UiBuilder.HasConfigUi ?? false; + if (hasMainUi) { ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowUpRightFromSquare, Locs.PluginButton_OpenUi)) + { + try + { + plugin.DalamudInterface.UiBuilder.OpenMain(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error during OpenMain(): {plugin.Name}"); + } + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(Locs.PluginButtonToolTip_OpenUi); + } + } + + if (hasConfig) + { + if (hasMainUi) + { + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(5, 0); + } + + ImGui.SameLine(); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Cog, Locs.PluginButton_OpenSettings)) { try { @@ -2648,7 +2682,7 @@ internal class PluginInstallerWindow : Window, IDisposable } catch (Exception ex) { - Log.Error(ex, $"Error during OpenConfigUi: {plugin.Name}"); + Log.Error(ex, $"Error during OpenConfig: {plugin.Name}"); } } @@ -3236,12 +3270,18 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButton_Unload => Loc.Localize("InstallerUnload", "Unload"); public static string PluginButton_SafeMode => Loc.Localize("InstallerSafeModeButton", "Can't change in safe mode"); + + public static string PluginButton_OpenUi => Loc.Localize("InstallerOpenPluginUi", "Open"); + + public static string PluginButton_OpenSettings => Loc.Localize("InstallerOpenPluginSettings", "Settings"); #endregion #region Plugin button tooltips + + public static string PluginButtonToolTip_OpenUi => Loc.Localize("InstallerTooltipOpenUi", "Open this plugin's interface"); - public static string PluginButtonToolTip_OpenConfiguration => Loc.Localize("InstallerOpenConfig", "Open Configuration"); + public static string PluginButtonToolTip_OpenConfiguration => Loc.Localize("InstallerTooltipOpenConfig", "Open this plugin's settings"); public static string PluginButtonToolTip_PickProfiles => Loc.Localize("InstallerPickProfiles", "Pick collections for this plugin"); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index eca0f64a0..b440a0705 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -69,6 +69,11 @@ public sealed class UiBuilder : IDisposable /// Event that is fired when the plugin should open its configuration interface. /// public event Action OpenConfigUi; + + /// + /// Event that is fired when the plugin should open its main interface. + /// + public event Action OpenMainUi; /// /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
@@ -212,6 +217,11 @@ public sealed class UiBuilder : IDisposable ///
internal bool HasConfigUi => this.OpenConfigUi != null; + /// + /// Gets a value indicating whether this UiBuilder has a configuration UI registered. + /// + internal bool HasMainUi => this.OpenMainUi != null; + /// /// Gets or sets the time this plugin took to draw on the last frame. /// @@ -409,6 +419,14 @@ public sealed class UiBuilder : IDisposable { this.OpenConfigUi?.InvokeSafely(); } + + /// + /// Open the registered configuration UI, if it exists. + /// + internal void OpenMain() + { + this.OpenMainUi?.InvokeSafely(); + } /// /// Notify this UiBuilder about plugin UI being hidden. From 122f4a462b293b02c73c1dec954198df8610ad6b Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 22 Aug 2023 22:24:27 +0200 Subject: [PATCH 19/40] build: 7.11.0.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index be04c5a20..af785cf52 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.10.2.0 + 7.11.0.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 84cd209d86ddf62fc6f76165c8132312e73c5281 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 20:48:57 +0200 Subject: [PATCH 20/40] chore: increase size of profiles tutorial --- .../Internal/Windows/PluginInstaller/ProfileManagerWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 301e43473..835a8a60c 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -78,7 +78,7 @@ internal class ProfileManagerWidget private void DrawTutorial(string modalTitle) { var open = true; - ImGui.SetNextWindowSize(new Vector2(450, 350), ImGuiCond.Appearing); + ImGui.SetNextWindowSize(new Vector2(650, 550), ImGuiCond.Appearing); using (var popup = ImRaii.PopupModal(modalTitle, ref open)) { if (popup) From d7f0f5d88844e0778756aad9c9c4a7f9164cad94 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 21:37:33 +0200 Subject: [PATCH 21/40] feat: add ColorHelpers class, various tools to manipulate HSV colors --- Dalamud/Interface/ColorHelpers.cs | 261 ++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 Dalamud/Interface/ColorHelpers.cs diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs new file mode 100644 index 000000000..71f959292 --- /dev/null +++ b/Dalamud/Interface/ColorHelpers.cs @@ -0,0 +1,261 @@ +using System; +using System.Numerics; + +namespace Dalamud.Interface; + +/// +/// Class containing various methods for manipulating colors. +/// +public static class ColorHelpers +{ + /// + /// Pack a vector4 color into a uint for use in ImGui APIs. + /// + /// The color to pack. + /// The packed color. + public static uint RgbaVector4ToUint(Vector4 color) + { + var r = (byte)(color.X * 255); + var g = (byte)(color.Y * 255); + var b = (byte)(color.Z * 255); + var a = (byte)(color.W * 255); + + return (uint)((a << 24) | (b << 16) | (g << 8) | r); + } + + /// + /// Convert a RGBA color in the range of 0.f to 1.f to a uint. + /// + /// The color to pack. + /// The packed color. + public static Vector4 RgbaUintToVector4(uint color) + { + var r = (color & 0x000000FF) / 255f; + var g = ((color & 0x0000FF00) >> 8) / 255f; + var b = ((color & 0x00FF0000) >> 16) / 255f; + var a = ((color & 0xFF000000) >> 24) / 255f; + + return new Vector4(r, g, b, a); + } + + /// + /// Convert a RGBA color in the range of 0.f to 1.f to a HSV color. + /// + /// The color to convert. + /// The color in a HSV representation. + public static HsvaColor RgbaToHsv(Vector4 color) + { + var r = color.X; + var g = color.Y; + var b = color.Z; + + var max = Math.Max(r, Math.Max(g, b)); + var min = Math.Min(r, Math.Min(g, b)); + + var h = max; + var s = max; + var v = max; + + var d = max - min; + s = max == 0 ? 0 : d / max; + + if (max == min) + { + h = 0; // achromatic + } + else + { + if (max == r) + { + h = ((g - b) / d) + (g < b ? 6 : 0); + } + else if (max == g) + { + h = ((b - r) / d) + 2; + } + else if (max == b) + { + h = ((r - g) / d) + 4; + } + + h /= 6; + } + + return new HsvaColor(h, s, v, color.W); + } + + /// + /// Convert a HSV color to a RGBA color in the range of 0.f to 1.f. + /// + /// The color to convert. + /// The RGB color. + public static Vector4 HsvToRgb(HsvaColor hsv) + { + var h = hsv.H; + var s = hsv.S; + var v = hsv.V; + + var r = 0f; + var g = 0f; + var b = 0f; + + var i = (int)Math.Floor(h * 6); + var f = (h * 6) - i; + var p = v * (1 - s); + var q = v * (1 - (f * s)); + var t = v * (1 - ((1 - f) * s)); + + switch (i % 6) + { + case 0: + r = v; + g = t; + b = p; + break; + + case 1: + r = q; + g = v; + b = p; + break; + + case 2: + r = p; + g = v; + b = t; + break; + + case 3: + r = p; + g = q; + b = v; + break; + + case 4: + r = t; + g = p; + b = v; + break; + + case 5: + r = v; + g = p; + b = q; + break; + } + + return new Vector4(r, g, b, hsv.A); + } + + /// + /// Lighten a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The lightened color. + public static Vector4 Lighten(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.V += amount; + return HsvToRgb(hsv); + } + + /// + /// Lighten a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The lightened color. + public static uint Lighten(uint color, float amount) + => RgbaVector4ToUint(Lighten(RgbaUintToVector4(color), amount)); + + /// + /// Darken a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The darkened color. + public static Vector4 Darken(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.V -= amount; + return HsvToRgb(hsv); + } + + /// + /// Darken a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The darkened color. + public static uint Darken(uint color, float amount) + => RgbaVector4ToUint(Darken(RgbaUintToVector4(color), amount)); + + /// + /// Saturate a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The saturated color. + public static Vector4 Saturate(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.S += amount; + return HsvToRgb(hsv); + } + + /// + /// Saturate a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The saturated color. + public static uint Saturate(uint color, float amount) + => RgbaVector4ToUint(Saturate(RgbaUintToVector4(color), amount)); + + /// + /// Desaturate a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The desaturated color. + public static Vector4 Desaturate(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.S -= amount; + return HsvToRgb(hsv); + } + + /// + /// Desaturate a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The desaturated color. + public static uint Desaturate(uint color, float amount) + => RgbaVector4ToUint(Desaturate(RgbaUintToVector4(color), amount)); + + /// + /// Fade a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The faded color. + public static Vector4 Fade(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.A -= amount; + return HsvToRgb(hsv); + } + + /// + /// Fade a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The faded color. + public static uint Fade(uint color, float amount) + => RgbaVector4ToUint(Fade(RgbaUintToVector4(color), amount)); + + public record struct HsvaColor(float H, float S, float V, float A); +} From d2e463247cd2c12e48dba36b1943e6776de0fc6f Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 21:40:22 +0200 Subject: [PATCH 22/40] chore: slightly tweak available plugin buttons --- .../PluginInstaller/PluginInstallerWindow.cs | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index ad9d77754..2e2830581 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1437,7 +1437,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.Button($"{buttonText}##{buttonText}testing"); } - this.DrawVisitRepoUrlButton("https://google.com"); + this.DrawVisitRepoUrlButton("https://google.com", true); if (this.testerImages != null) { @@ -1954,6 +1954,7 @@ internal class PluginInstallerWindow : Window, IDisposable } else { + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.DalamudRed.Darken(0.3f).Fade(0.4f)); var buttonText = Locs.PluginButton_InstallVersion(versionString); if (ImGui.Button($"{buttonText}##{buttonText}{index}")) { @@ -1961,11 +1962,19 @@ internal class PluginInstallerWindow : Window, IDisposable } } - this.DrawVisitRepoUrlButton(manifest.RepoUrl); + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(10); + ImGui.SameLine(); + + this.DrawVisitRepoUrlButton(manifest.RepoUrl, true); + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(3); + ImGui.SameLine(); if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback) { - this.DrawSendFeedbackButton(manifest, false); + this.DrawSendFeedbackButton(manifest, false, true); } ImGuiHelpers.ScaledDummy(5); @@ -2235,12 +2244,12 @@ internal class PluginInstallerWindow : Window, IDisposable // Controls this.DrawPluginControlButton(plugin, availablePluginUpdate); this.DrawDevPluginButtons(plugin); + this.DrawVisitRepoUrlButton(plugin.Manifest.RepoUrl, false); this.DrawDeletePluginButton(plugin); - this.DrawVisitRepoUrlButton(plugin.Manifest.RepoUrl); if (canFeedback) { - this.DrawSendFeedbackButton(plugin.Manifest, plugin.IsTesting); + this.DrawSendFeedbackButton(plugin.Manifest, plugin.IsTesting, false); } if (availablePluginUpdate != default && !plugin.IsDev) @@ -2693,10 +2702,15 @@ internal class PluginInstallerWindow : Window, IDisposable } } - private void DrawSendFeedbackButton(IPluginManifest manifest, bool isTesting) + private void DrawSendFeedbackButton(IPluginManifest manifest, bool isTesting, bool big) { ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Comment)) + + var clicked = big ? + ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Comment, Locs.FeedbackModal_Title) : + ImGuiComponents.IconButton(FontAwesomeIcon.Comment); + + if (clicked) { this.feedbackPlugin = manifest; this.feedbackModalOnNextFrame = true; @@ -2842,12 +2856,16 @@ internal class PluginInstallerWindow : Window, IDisposable } } - private void DrawVisitRepoUrlButton(string? repoUrl) + private void DrawVisitRepoUrlButton(string? repoUrl, bool big) { if (!string.IsNullOrEmpty(repoUrl) && repoUrl.StartsWith("https://")) { ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Globe)) + + var clicked = big ? + ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Globe, "Open website") : + ImGuiComponents.IconButton(FontAwesomeIcon.Globe); + if (clicked) { try { From 3c8e474fe506254a2891839ba8293d1057d58940 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 21:51:46 +0200 Subject: [PATCH 23/40] fix: internal TSM entries must always come first --- .../Internal/Windows/TitleScreenMenuWindow.cs | 17 +++++++++-------- Dalamud/Interface/TitleScreenMenu.cs | 15 +++++++++++++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 10180f0c3..8c835c76a 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -97,16 +97,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable public override void Draw() { var scale = ImGui.GetIO().FontGlobalScale; - - var tsm = Service.Get(); + var entries = Service.Get().Entries + .OrderByDescending(x => x.IsInternal) + .ToList(); switch (this.state) { case State.Show: { - for (var i = 0; i < tsm.Entries.Count; i++) + for (var i = 0; i < entries.Count; i++) { - var entry = tsm.Entries[i]; + var entry = entries[i]; if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing)) { @@ -172,9 +173,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value)) { - for (var i = 0; i < tsm.Entries.Count; i++) + for (var i = 0; i < entries.Count; i++) { - var entry = tsm.Entries[i]; + var entry = entries[i]; var finalPos = (i + 1) * this.shadeTexture.Height * scale; @@ -205,7 +206,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable case State.Hide: { - if (this.DrawEntry(tsm.Entries[0], true, false, true, true, false)) + if (this.DrawEntry(entries[0], true, false, true, true, false)) { this.state = State.Show; } @@ -217,7 +218,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable } } - var srcText = tsm.Entries.Select(e => e.Name).ToHashSet(); + var srcText = entries.Select(e => e.Name).ToHashSet(); var keys = this.specialGlyphRequests.Keys.ToHashSet(); keys.RemoveWhere(x => srcText.Contains(x)); foreach (var key in keys) diff --git a/Dalamud/Interface/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu.cs index 7b3897fdb..c9e1458d6 100644 --- a/Dalamud/Interface/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu.cs @@ -121,7 +121,10 @@ public class TitleScreenMenu : IServiceType lock (this.entries) { - var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered); + var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered) + { + IsInternal = true, + }; this.entries.Add(entry); return entry; } @@ -148,7 +151,10 @@ public class TitleScreenMenu : IServiceType var priority = entriesOfAssembly.Any() ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) : 0; - var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered); + var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered) + { + IsInternal = true, + }; this.entries.Add(entry); return entry; } @@ -192,6 +198,11 @@ public class TitleScreenMenu : IServiceType /// Gets or sets the texture of this entry. /// public TextureWrap Texture { get; set; } + + /// + /// Gets or sets a value indicating whether or not this entry is internal. + /// + internal bool IsInternal { get; set; } /// /// Gets the calling assembly of this entry. From 4c177d32b4bc84260fb41915a40f6950f46d7198 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 22:01:57 +0200 Subject: [PATCH 24/40] fix: ensure that toggle button stays enabled while enabling --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 2e2830581..27bf83dbd 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -108,7 +108,9 @@ internal class PluginInstallerWindow : Window, IDisposable private OperationStatus installStatus = OperationStatus.Idle; private OperationStatus updateStatus = OperationStatus.Idle; + private OperationStatus enableDisableStatus = OperationStatus.Idle; + private Guid enableDisableWorkingPluginId = Guid.Empty; private LoadingIndicatorKind loadingIndicatorKind = LoadingIndicatorKind.Unknown; @@ -2477,6 +2479,10 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.PluginButtonToolTip_UnloadFailed); } + else if (this.enableDisableStatus == OperationStatus.InProgress && this.enableDisableWorkingPluginId == plugin.Manifest.WorkingPluginId) + { + ImGuiComponents.DisabledToggleButton(toggleId, this.loadingIndicatorKind == LoadingIndicatorKind.EnablingSingle); + } else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled) { ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable); @@ -2516,6 +2522,7 @@ internal class PluginInstallerWindow : Window, IDisposable { this.enableDisableStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.DisablingSingle; + this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; Task.Run(async () => { @@ -2536,6 +2543,7 @@ internal class PluginInstallerWindow : Window, IDisposable { this.enableDisableStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; + this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; await applicableProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false); await plugin.LoadAsync(PluginLoadReason.Installer); From 2d528f0515eca59e7e4a58df0ac45d6c3b004e26 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 25 Aug 2023 12:49:32 -0700 Subject: [PATCH 25/40] Update SettingsTabExperimental.cs (#1355) --- .../Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index 62981f4a2..15dab7d94 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -25,7 +25,7 @@ public class SettingsTabExperimental : SettingsTab c => c.DoPluginTest, (v, c) => c.DoPluginTest = v), new HintSettingsEntry( - Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may not have been vetted before being published. Please only enable this if you are aware of the risks."), + Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."), ImGuiColors.DalamudRed), new GapSettingsEntry(5), From 3afe9c41a552568b2f1c390d77e262b3283813f1 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 25 Aug 2023 21:50:20 +0200 Subject: [PATCH 26/40] Update ClientStructs (#1354) 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 de4c356bf..b7ef3eaf0 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit de4c356bf27da2e3d596478c0192ce0baa75c125 +Subproject commit b7ef3eaf02334aad36da26ff25f5f70dee411894 From 1ab198af8bbfcb4fbdcf61ed1bb181071b920c0b Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 26 Aug 2023 19:53:16 +0200 Subject: [PATCH 27/40] Update ClientStructs (#1358) 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 b7ef3eaf0..1e52770d5 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b7ef3eaf02334aad36da26ff25f5f70dee411894 +Subproject commit 1e52770d5367c8237ac78688774d136118994bf7 From f7c2ab528338237ae123ffaa3e3e52daeb7f9184 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 27 Aug 2023 14:46:44 +0200 Subject: [PATCH 28/40] Support Unicode names correctly. --- Dalamud/Interface/DragDrop/DragDropInterop.cs | 2 +- Dalamud/Interface/DragDrop/DragDropTarget.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs index 28a2644a5..68418d4b0 100644 --- a/Dalamud/Interface/DragDrop/DragDropInterop.cs +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -101,7 +101,7 @@ internal partial class DragDropManager public static extern int RevokeDragDrop(nint hwnd); [DllImport("shell32.dll")] - public static extern int DragQueryFile(IntPtr hDrop, uint iFile, StringBuilder lpszFile, int cch); + public static extern int DragQueryFileW(IntPtr hDrop, uint iFile, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpszFile, int cch); } } #pragma warning restore SA1600 // Elements should be documented diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index 5e7166fb3..01a48173a 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -204,7 +204,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget try { data.GetData(ref this.formatEtc, out var stgMedium); - var numFiles = DragDropInterop.DragQueryFile(stgMedium.unionmember, uint.MaxValue, new StringBuilder(), 0); + var numFiles = DragDropInterop.DragQueryFileW(stgMedium.unionmember, uint.MaxValue, new StringBuilder(), 0); var files = new string[numFiles]; var sb = new StringBuilder(1024); var directoryCount = 0; @@ -212,11 +212,11 @@ internal partial class DragDropManager : DragDropManager.IDropTarget for (var i = 0u; i < numFiles; ++i) { sb.Clear(); - var ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity); + var ret = DragDropInterop.DragQueryFileW(stgMedium.unionmember, i, sb, sb.Capacity); if (ret >= sb.Capacity) { sb.Capacity = ret + 1; - ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity); + ret = DragDropInterop.DragQueryFileW(stgMedium.unionmember, i, sb, sb.Capacity); } if (ret > 0 && ret < sb.Capacity) From 49bffccf82884eb1c73c4adf79f0eaaaca46c7a9 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:49:06 +0200 Subject: [PATCH 29/40] Update ClientStructs (#1359) 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 1e52770d5..dcc61941b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 1e52770d5367c8237ac78688774d136118994bf7 +Subproject commit dcc61941b2bd73b1aa5badb40276265b80e91a69 From efefcd70cfdc8a9e95ff763543efe40cc16a0b0e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:56:31 -0700 Subject: [PATCH 30/40] Add AddonLifecycle Service (#1361) Adds new service to manage addon lifecycle events, such as setup/teardown. --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 169 ++++++++++++++++++ .../AddonLifecycleAddressResolver.cs | 27 +++ Dalamud/Plugin/Services/IAddonLifecycle.cs | 50 ++++++ 3 files changed, 246 insertions(+) create mode 100644 Dalamud/Game/AddonLifecycle/AddonLifecycle.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs create mode 100644 Dalamud/Plugin/Services/IAddonLifecycle.cs diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs new file mode 100644 index 000000000..95cb2539c --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -0,0 +1,169 @@ +using System; +using System.Runtime.InteropServices; + +using Dalamud.Hooking; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonLifecycle; + +/// +/// This class provides events for in-game addon lifecycles. +/// +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycle +{ + private static readonly ModuleLog Log = new("AddonLifecycle"); + private readonly AddonLifecycleAddressResolver address; + private readonly Hook onAddonSetupHook; + private readonly Hook onAddonFinalizeHook; + + [ServiceManager.ServiceConstructor] + private AddonLifecycle(SigScanner sigScanner) + { + this.address = new AddonLifecycleAddressResolver(); + this.address.Setup(sigScanner); + + this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); + this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonFinalize); + } + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate nint AddonSetupDelegate(AtkUnitBase* addon); + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + + /// + public event Action? AddonPreSetup; + + /// + public event Action? AddonPostSetup; + + /// + public event Action? AddonPreFinalize; + + /// + public event Action? AddonPostFinalize; + + /// + public void Dispose() + { + this.onAddonSetupHook.Dispose(); + this.onAddonFinalizeHook.Dispose(); + } + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction() + { + this.onAddonSetupHook.Enable(); + this.onAddonFinalizeHook.Enable(); + } + + private nint OnAddonSetup(AtkUnitBase* addon) + { + try + { + this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonSetup pre-setup invoke."); + } + + var result = this.onAddonSetupHook.Original(addon); + + try + { + this.AddonPostSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonSetup post-setup invoke."); + } + + return result; + } + + private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) + { + try + { + this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke."); + } + + this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); + + try + { + this.AddonPostFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonFinalize post-finalize invoke."); + } + } +} + +/// +/// Plugin-scoped version of a AddonLifecycle service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle +{ + [ServiceManager.ServiceDependency] + private readonly AddonLifecycle addonLifecycleService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + public AddonLifecyclePluginScoped() + { + this.addonLifecycleService.AddonPreSetup += this.AddonPreSetupForward; + this.addonLifecycleService.AddonPostSetup += this.AddonPostSetupForward; + this.addonLifecycleService.AddonPreFinalize += this.AddonPreFinalizeForward; + this.addonLifecycleService.AddonPostFinalize += this.AddonPostFinalizeForward; + } + + /// + public event Action? AddonPreSetup; + + /// + public event Action? AddonPostSetup; + + /// + public event Action? AddonPreFinalize; + + /// + public event Action? AddonPostFinalize; + + /// + public void Dispose() + { + this.addonLifecycleService.AddonPreSetup -= this.AddonPreSetupForward; + this.addonLifecycleService.AddonPostSetup -= this.AddonPostSetupForward; + this.addonLifecycleService.AddonPreFinalize -= this.AddonPreFinalizeForward; + this.addonLifecycleService.AddonPostFinalize -= this.AddonPostFinalizeForward; + } + + private void AddonPreSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPreSetup?.Invoke(args); + + private void AddonPostSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPostSetup?.Invoke(args); + + private void AddonPreFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPreFinalize?.Invoke(args); + + private void AddonPostFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPostFinalize?.Invoke(args); +} diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs new file mode 100644 index 000000000..ba7b723ec --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.AddonLifecycle; + +/// +/// AddonLifecycleService memory address resolver. +/// +internal class AddonLifecycleAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the addon setup hook invoked by the atkunitmanager. + /// + public nint AddonSetup { get; private set; } + + /// + /// Gets the address of the addon finalize hook invoked by the atkunitmanager. + /// + public nint AddonFinalize { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(SigScanner sig) + { + this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); + this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); + } +} diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs new file mode 100644 index 000000000..7b90cf0cd --- /dev/null +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -0,0 +1,50 @@ +using System; + +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Plugin.Services; + +/// +/// This class provides events for in-game addon lifecycles. +/// +public interface IAddonLifecycle +{ + /// + /// Event that fires before an addon is being setup. + /// + public event Action AddonPreSetup; + + /// + /// Event that fires after an addon is done being setup. + /// + public event Action AddonPostSetup; + + /// + /// Event that fires before an addon is being finalized. + /// + public event Action AddonPreFinalize; + + /// + /// Event that fires after an addon is done being finalized. + /// + public event Action AddonPostFinalize; + + /// + /// Addon argument data for use in event subscribers. + /// + public unsafe class AddonArgs + { + private string? addonName; + + /// + /// Gets the name of the addon this args referrers to. + /// + public string AddonName => this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + required public nint Addon { get; init; } + } +} From cfef50af0c9ac5a64e1b08ff706b03ae793def67 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:01:53 -0700 Subject: [PATCH 31/40] [AddonLifecycle] Fixes --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 33 ++++++------------- Dalamud/Plugin/Services/IAddonLifecycle.cs | 7 +--- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 95cb2539c..c3ec038ed 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; using Dalamud.Hooking; using Dalamud.IoC; @@ -29,13 +28,11 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl this.address.Setup(sigScanner); this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); - this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonFinalize); + this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); } - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate nint AddonSetupDelegate(AtkUnitBase* addon); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); /// @@ -47,9 +44,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl /// public event Action? AddonPreFinalize; - /// - public event Action? AddonPostFinalize; - /// public void Dispose() { @@ -66,6 +60,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl private nint OnAddonSetup(AtkUnitBase* addon) { + if (addon is null) + return this.onAddonSetupHook.Original(addon); + try { this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); @@ -91,6 +88,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { + if (atkUnitBase is null) + { + this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); + return; + } + try { this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); @@ -101,15 +104,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl } this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); - - try - { - this.AddonPostFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonFinalize post-finalize invoke."); - } } } @@ -135,7 +129,6 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif this.addonLifecycleService.AddonPreSetup += this.AddonPreSetupForward; this.addonLifecycleService.AddonPostSetup += this.AddonPostSetupForward; this.addonLifecycleService.AddonPreFinalize += this.AddonPreFinalizeForward; - this.addonLifecycleService.AddonPostFinalize += this.AddonPostFinalizeForward; } /// @@ -147,16 +140,12 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif /// public event Action? AddonPreFinalize; - /// - public event Action? AddonPostFinalize; - /// public void Dispose() { this.addonLifecycleService.AddonPreSetup -= this.AddonPreSetupForward; this.addonLifecycleService.AddonPostSetup -= this.AddonPostSetupForward; this.addonLifecycleService.AddonPreFinalize -= this.AddonPreFinalizeForward; - this.addonLifecycleService.AddonPostFinalize -= this.AddonPostFinalizeForward; } private void AddonPreSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPreSetup?.Invoke(args); @@ -164,6 +153,4 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif private void AddonPostSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPostSetup?.Invoke(args); private void AddonPreFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPreFinalize?.Invoke(args); - - private void AddonPostFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPostFinalize?.Invoke(args); } diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 7b90cf0cd..43b9fef0a 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -25,11 +25,6 @@ public interface IAddonLifecycle /// public event Action AddonPreFinalize; - /// - /// Event that fires after an addon is done being finalized. - /// - public event Action AddonPostFinalize; - /// /// Addon argument data for use in event subscribers. /// @@ -40,7 +35,7 @@ public interface IAddonLifecycle /// /// Gets the name of the addon this args referrers to. /// - public string AddonName => this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); /// /// Gets the pointer to the addons AtkUnitBase. From 46fbb94b1ddc428c181d78ea6004118f9ea5403b Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:05:31 -0700 Subject: [PATCH 32/40] [AddonLifecycle] Also check specific addon for null --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index c3ec038ed..72d1c25ff 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -88,7 +88,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { - if (atkUnitBase is null) + if (atkUnitBase is null || atkUnitBase[0] is null) { this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); return; From 8a267e51bf588e687cffab49285f65a53be1c6ca Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 30 Aug 2023 23:53:24 +0200 Subject: [PATCH 33/40] feat: improve custom repo disclaimer a bit --- .../Internal/DalamudConfiguration.cs | 5 ++ .../Windows/Settings/SettingsEntry.cs | 8 +++ .../Internal/Windows/Settings/SettingsTab.cs | 5 +- .../Widgets/ThirdRepoSettingsEntry.cs | 61 ++++++++++++++++++- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index ac410527c..2d0a08942 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -100,6 +100,11 @@ internal sealed class DalamudConfiguration : IServiceType /// public List ThirdRepoList { get; set; } = new(); + /// + /// Gets or sets a value indicating whether or not a disclaimer regarding third-party repos has been dismissed. + /// + public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null; + /// /// Gets or sets a list of hidden plugins. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs index 5e1dc7884..1e57d716e 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs @@ -42,6 +42,14 @@ public abstract class SettingsEntry /// public abstract void Draw(); + /// + /// Function to be called when the tab is opened. + /// + public virtual void OnOpen() + { + // ignored + } + /// /// Function to be called when the tab is closed. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs index 16b7749cb..49a8935df 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs @@ -16,7 +16,10 @@ public abstract class SettingsTab : IDisposable public virtual void OnOpen() { - // ignored + foreach (var settingsEntry in this.Entries) + { + settingsEntry.OnOpen(); + } } public virtual void OnClose() diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs index be2e34a57..6e476792b 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Raii; using Dalamud.Plugin.Internal; +using Dalamud.Utility; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; @@ -23,7 +24,13 @@ public class ThirdRepoSettingsEntry : SettingsEntry private bool thirdRepoListChanged; private string thirdRepoTempUrl = string.Empty; private string thirdRepoAddError = string.Empty; + private DateTime timeSinceOpened; + public override void OnOpen() + { + this.timeSinceOpened = DateTime.Now; + } + public override void OnClose() { this.thirdRepoList = @@ -51,6 +58,8 @@ public class ThirdRepoSettingsEntry : SettingsEntry public override void Draw() { + var config = Service.Get(); + using var id = ImRaii.PushId("thirdRepo"); ImGui.TextUnformatted(Loc.Localize("DalamudSettingsCustomRepo", "Custom Plugin Repositories")); if (this.thirdRepoListChanged) @@ -61,13 +70,59 @@ public class ThirdRepoSettingsEntry : SettingsEntry ImGui.TextUnformatted(Loc.Localize("DalamudSettingsChanged", "(Changed)")); } } - + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingCustomRepoHint", "Add custom plugin repositories.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for third-party plugins and repositories.")); + + ImGuiHelpers.ScaledDummy(2); + + config.ThirdRepoSpeedbumpDismissed ??= config.ThirdRepoList.Any(x => x.IsEnabled); + var disclaimerDismissed = config.ThirdRepoSpeedbumpDismissed.Value; + + ImGui.PushFont(InterfaceManager.IconFont); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle.ToIconString()); + ImGui.PopFont(); + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(2); + ImGui.SameLine(); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarningReadThis", "READ THIS FIRST!")); + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(2); + ImGui.SameLine(); + ImGui.PushFont(InterfaceManager.IconFont); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle.ToIconString()); + ImGui.PopFont(); + + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for custom plugins and repositories.")); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning5", "If someone told you to copy/paste something here, it's very possible that you are being scammed or taken advantage of.")); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning2", "Plugins have full control over your PC, like any other program, and may cause harm or crashes.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning4", "They can delete your character, upload your family photos and burn down your house.")); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning4", "They can delete your character, steal your FC or Discord account, and burn down your house.")); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning3", "Please make absolutely sure that you only install third-party plugins from developers you trust.")); + if (!disclaimerDismissed) + { + const int speedbumpTime = 15; + var elapsed = DateTime.Now - this.timeSinceOpened; + if (elapsed < TimeSpan.FromSeconds(speedbumpTime)) + { + ImGui.BeginDisabled(); + ImGui.Button( + Loc.Localize("DalamudSettingCustomRepoWarningPleaseWait", "Please wait {0} seconds...").Format(speedbumpTime - elapsed.Seconds)); + ImGui.EndDisabled(); + } + else + { + if (ImGui.Button(Loc.Localize("DalamudSettingCustomRepoWarningIReadIt", "Ok, I have read and understood this warning"))) + { + config.ThirdRepoSpeedbumpDismissed = true; + config.QueueSave(); + } + } + } + + ImGuiHelpers.ScaledDummy(2); + + using var disabled = ImRaii.Disabled(!disclaimerDismissed); + ImGuiHelpers.ScaledDummy(5); ImGui.Columns(4); From 7a03458696756dd2baa4219850deccbf0e4f4ff0 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 7 Sep 2023 18:43:04 +0200 Subject: [PATCH 34/40] Update ClientStructs (#1363) 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 dcc61941b..ada62e7ae 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit dcc61941b2bd73b1aa5badb40276265b80e91a69 +Subproject commit ada62e7ae60de220d1f950b03ddb8d66e9e10daf From a12d9df9a20645c454d4b49c727fd432e8beba61 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:07:55 -0700 Subject: [PATCH 35/40] Chat Payload Remove Logspam (#1368) --- Dalamud/Game/Text/SeStringHandling/Payload.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs index dbd70a58e..117606a7a 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs @@ -206,9 +206,9 @@ public abstract partial class Payload case SeStringChunkType.Icon: payload = new IconPayload(); break; - + default: - Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType); + // Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType); break; } @@ -307,6 +307,11 @@ public abstract partial class Payload /// protected enum SeStringChunkType { + /// + /// See the . + /// + NewLine = 0x10, + /// /// See the class. /// @@ -317,11 +322,6 @@ public abstract partial class Payload /// EmphasisItalic = 0x1A, - /// - /// See the . - /// - NewLine = 0x10, - /// /// See the class. /// From 633894364d850569c2270b26690a62011fd6e5b9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:12:19 -0700 Subject: [PATCH 36/40] Add Targets to TargetManager (#1364) --- Dalamud/Game/ClientState/Objects/TargetManager.cs | 14 ++++++++++++++ .../Internal/Windows/Data/Widgets/TargetWidget.cs | 6 ++++++ Dalamud/Plugin/Services/ITargetManager.cs | 12 ++++++++++++ 3 files changed, 32 insertions(+) diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index ff1bdc5ba..00bcaac7d 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -70,6 +70,20 @@ public sealed unsafe class TargetManager : IServiceType, ITargetManager set => this.SetSoftTarget(value); } + /// + public GameObject? GPoseTarget + { + get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget); + set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; + } + + /// + public GameObject? MouseOverNameplateTarget + { + get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverNameplateTarget); + set => Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; + } + private FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem*)this.Address; /// diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index 57fd03300..f33e67f70 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -63,6 +63,12 @@ internal class TargetWidget : IDataWindowWidget if (targetMgr.SoftTarget != null) Util.PrintGameObject(targetMgr.SoftTarget, "SoftTarget", this.resolveGameData); + + if (targetMgr.GPoseTarget != null) + Util.PrintGameObject(targetMgr.GPoseTarget, "GPoseTarget", this.resolveGameData); + + if (targetMgr.MouseOverNameplateTarget != null) + Util.PrintGameObject(targetMgr.MouseOverNameplateTarget, "MouseOverNameplateTarget", this.resolveGameData); if (ImGui.Button("Clear CT")) targetMgr.Target = null; diff --git a/Dalamud/Plugin/Services/ITargetManager.cs b/Dalamud/Plugin/Services/ITargetManager.cs index 108b1ca03..99a9d8dfb 100644 --- a/Dalamud/Plugin/Services/ITargetManager.cs +++ b/Dalamud/Plugin/Services/ITargetManager.cs @@ -41,4 +41,16 @@ public interface ITargetManager /// Set to null to clear the target. /// public GameObject? SoftTarget { get; set; } + + /// + /// Gets or sets the gpose target. + /// Set to null to clear the target. + /// + public GameObject? GPoseTarget { get; set; } + + /// + /// Gets or sets the mouseover nameplate target. + /// Set to null to clear the target. + /// + public GameObject? MouseOverNameplateTarget { get; set; } } From 37cb1f5dd075a47bd819625e8dc6011254ddf3d7 Mon Sep 17 00:00:00 2001 From: Kurochi51 Date: Thu, 7 Sep 2023 20:33:10 +0300 Subject: [PATCH 37/40] Fade to black during credits animation (#1356) --- Dalamud/Interface/Internal/DalamudInterface.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 479297c20..a8a769070 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -524,7 +524,8 @@ internal class DalamudInterface : IDisposable, IServiceType private void DrawCreditsDarkeningAnimation() { - using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f); + using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding | ImGuiStyleVar.WindowBorderSize, 0f); + using var color = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 0)); ImGui.SetNextWindowPos(new Vector2(0, 0)); ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size); From 1dbf93e428fdc4a5d324b281678b4c5befe179d7 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 7 Sep 2023 10:33:46 -0700 Subject: [PATCH 38/40] Add SpecialPluginSource to public API (#1357) --- Dalamud/Plugin/DalamudPluginInterface.cs | 6 +++--- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- .../Types/Manifest/LocalPluginManifest.cs | 14 +------------- .../Types/Manifest/SpecialPluginSource.cs | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 Dalamud/Plugin/Internal/Types/Manifest/SpecialPluginSource.cs diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 2b58c21cc..7d788726d 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -56,7 +56,7 @@ public sealed class DalamudPluginInterface : IDisposable this.configs = Service.Get().PluginConfigs; this.Reason = reason; - this.SourceRepository = this.IsDev ? LocalPluginManifest.FlagDevPlugin : plugin.Manifest.InstalledFromUrl; + this.SourceRepository = this.IsDev ? SpecialPluginSource.DevPlugin : plugin.Manifest.InstalledFromUrl; this.IsTesting = plugin.IsTesting; this.LoadTime = DateTime.Now; @@ -118,8 +118,8 @@ public sealed class DalamudPluginInterface : IDisposable /// Gets the repository from which this plugin was installed. /// /// If a plugin was installed from the official/main repository, this will return the value of - /// . Developer plugins will return the value of - /// . + /// . Developer plugins will return the value of + /// . /// public string SourceRepository { get; } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 58e122c3e..e66f226ea 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -873,7 +873,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.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty ? repoManifest.SourceRepo.PluginMasterUrl : SpecialPluginSource.MainRepo; manifest.Save(manifestFile, "installation"); diff --git a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs index 8afbe1aea..b7fe6d062 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs @@ -13,18 +13,6 @@ namespace Dalamud.Plugin.Internal.Types.Manifest; /// internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest { - /// - /// Flag indicating that a plugin was installed from the official repo. - /// - [JsonIgnore] - public const string FlagMainRepo = "OFFICIAL"; - - /// - /// Flag indicating that a plugin is a dev plugin.. - /// - [JsonIgnore] - public const string FlagDevPlugin = "DEVPLUGIN"; - /// /// Gets or sets a value indicating whether the plugin is disabled and should not be loaded. /// This value supersedes the ".disabled" file functionality and should not be included in the plugin master. @@ -51,7 +39,7 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest /// 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. /// - public bool IsThirdParty => !this.InstalledFromUrl.IsNullOrEmpty() && this.InstalledFromUrl != FlagMainRepo; + public bool IsThirdParty => !this.InstalledFromUrl.IsNullOrEmpty() && this.InstalledFromUrl != SpecialPluginSource.MainRepo; /// /// Gets the effective version of this plugin. diff --git a/Dalamud/Plugin/Internal/Types/Manifest/SpecialPluginSource.cs b/Dalamud/Plugin/Internal/Types/Manifest/SpecialPluginSource.cs new file mode 100644 index 000000000..d6508019d --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/Manifest/SpecialPluginSource.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Plugin.Internal.Types.Manifest; + +/// +/// A fake enum representing "special" sources for plugins. +/// +public static class SpecialPluginSource +{ + /// + /// Indication that this plugin came from the official Dalamud repository. + /// + public const string MainRepo = "OFFICIAL"; + + /// + /// Indication that this plugin is loaded as a dev plugin. See also . + /// + public const string DevPlugin = "DEVPLUGIN"; +} From 8c51bbf0f8140913a88deee072134200a06540c5 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 7 Sep 2023 10:58:41 -0700 Subject: [PATCH 39/40] Add Scoped Plugin Log Service (#1341) Adds a new `IPluginLog` service to Dalamud, which provides scoped logging on a per-plugin basis. This improves log performance for plugins, and paves the way for per-plugin log levels. * Plugins must opt in to enable verbose logging by setting `IPluginLog.MinimumLogLevel` to `LogEventLevel.Verbose`. This option is automatically enabled for dev plugins and is currently not persisted. * All release plugins will default to `Debug` as their lowest allowed log level. * This setting does not override the global log level set in Dalamud. --- Dalamud.CorePlugin/PluginImpl.cs | 3 +- Dalamud/Logging/PluginLog.cs | 69 +----------- Dalamud/Logging/ScopedPluginLogService.cs | 130 ++++++++++++++++++++++ Dalamud/Plugin/Services/IPluginLog.cs | 128 +++++++++++++++++++++ 4 files changed, 263 insertions(+), 67 deletions(-) create mode 100644 Dalamud/Logging/ScopedPluginLogService.cs create mode 100644 Dalamud/Plugin/Services/IPluginLog.cs diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index b858e9a0c..2f76a1087 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -6,6 +6,7 @@ using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -56,7 +57,7 @@ namespace Dalamud.CorePlugin /// /// Dalamud plugin interface. /// Logging service. - public PluginImpl(DalamudPluginInterface pluginInterface, PluginLog log) + public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log) { try { diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index acbd663e7..b2f2a5065 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -1,9 +1,6 @@ using System; using System.Reflection; -using Dalamud.IoC; -using Dalamud.IoC.Internal; -using Dalamud.Plugin.Internal.Types; using Serilog; using Serilog.Events; @@ -12,29 +9,9 @@ namespace Dalamud.Logging; /// /// Class offering various static methods to allow for logging in plugins. /// -[PluginInterface] -[InterfaceVersion("1.0")] -[ServiceManager.ScopedService] -public class PluginLog : IServiceType, IDisposable +public static class PluginLog { - private readonly LocalPlugin plugin; - - /// - /// Initializes a new instance of the class. - /// Do not use this ctor, inject PluginLog instead. - /// - /// The plugin this service is scoped for. - internal PluginLog(LocalPlugin plugin) - { - this.plugin = plugin; - } - - /// - /// Gets or sets a prefix appended to log messages. - /// - public string? LogPrefix { get; set; } = null; - - #region Legacy static "Log" prefixed Serilog style methods + #region "Log" prefixed Serilog style methods /// /// Log a templated message to the in-game debug log. @@ -157,7 +134,7 @@ public class PluginLog : IServiceType, IDisposable #endregion - #region Legacy static Serilog style methods + #region Serilog style methods /// /// Log a templated verbose message to the in-game debug log. @@ -277,25 +254,6 @@ public class PluginLog : IServiceType, IDisposable public static void LogRaw(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values) => WriteLog(Assembly.GetCallingAssembly().GetName().Name, level, messageTemplate, exception, values); - /// - void IDisposable.Dispose() - { - // ignored - } - - #region New instanced methods - - /// - /// Log some information. - /// - /// The message. - internal void Information(string message) - { - Serilog.Log.Information($"[{this.plugin.InternalName}] {this.LogPrefix} {message}"); - } - - #endregion - private static ILogger GetPluginLogger(string? pluginName) { return Serilog.Log.ForContext("SourceContext", pluginName ?? string.Empty); @@ -314,24 +272,3 @@ public class PluginLog : IServiceType, IDisposable values); } } - -/// -/// Class offering logging services, for a specific type. -/// -/// The type to log for. -[PluginInterface] -[InterfaceVersion("1.0")] -[ServiceManager.ScopedService] -public class PluginLog : PluginLog -{ - /// - /// Initializes a new instance of the class. - /// Do not use this ctor, inject PluginLog instead. - /// - /// The plugin this service is scoped for. - internal PluginLog(LocalPlugin plugin) - : base(plugin) - { - this.LogPrefix = typeof(T).Name; - } -} diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs new file mode 100644 index 000000000..8c502fcf0 --- /dev/null +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -0,0 +1,130 @@ +using System; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; +using Serilog; +using Serilog.Core; +using Serilog.Events; + +namespace Dalamud.Logging; + +/// +/// Implementation of . +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable +{ + private readonly LocalPlugin localPlugin; + + private readonly LoggingLevelSwitch levelSwitch; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin that owns this service. + internal ScopedPluginLogService(LocalPlugin localPlugin) + { + this.localPlugin = localPlugin; + + this.levelSwitch = new LoggingLevelSwitch(this.GetDefaultLevel()); + + var loggerConfiguration = new LoggerConfiguration() + .Enrich.WithProperty("Dalamud.PluginName", localPlugin.InternalName) + .MinimumLevel.ControlledBy(this.levelSwitch) + .WriteTo.Logger(Log.Logger); + + this.Logger = loggerConfiguration.CreateLogger(); + } + + /// + public LogEventLevel MinimumLogLevel + { + get => this.levelSwitch.MinimumLevel; + set => this.levelSwitch.MinimumLevel = value; + } + + /// + public ILogger Logger { get; } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + } + + /// + public void Fatal(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Fatal, null, messageTemplate, values); + + /// + public void Fatal(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Fatal, exception, messageTemplate, values); + + /// + public void Error(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Error, null, messageTemplate, values); + + /// + public void Error(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Error, exception, messageTemplate, values); + + /// + public void Warning(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Warning, null, messageTemplate, values); + + /// + public void Warning(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Warning, exception, messageTemplate, values); + + /// + public void Information(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Information, null, messageTemplate, values); + + /// + public void Information(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Information, exception, messageTemplate, values); + + /// + public void Debug(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Debug, null, messageTemplate, values); + + /// + public void Debug(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Debug, exception, messageTemplate, values); + + /// + public void Verbose(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Verbose, null, messageTemplate, values); + + /// + public void Verbose(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Verbose, exception, messageTemplate, values); + + /// + public void Write(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values) + { + this.Logger.Write( + level, + exception: exception, + messageTemplate: $"[{this.localPlugin.InternalName}] {messageTemplate}", + values); + } + + /// + /// Gets the default log level for this plugin. + /// + /// A log level. + private LogEventLevel GetDefaultLevel() + { + // TODO: Add some way to save log levels to a config. Or let plugins handle it? + + return this.localPlugin.IsDev ? LogEventLevel.Verbose : LogEventLevel.Debug; + } +} diff --git a/Dalamud/Plugin/Services/IPluginLog.cs b/Dalamud/Plugin/Services/IPluginLog.cs new file mode 100644 index 000000000..87876f36f --- /dev/null +++ b/Dalamud/Plugin/Services/IPluginLog.cs @@ -0,0 +1,128 @@ +using System; + +using Serilog; +using Serilog.Events; + +namespace Dalamud.Plugin.Services; + +/// +/// An opinionated service to handle logging for plugins. +/// +public interface IPluginLog +{ + /// + /// Gets or sets the minimum log level that will be recorded from this plugin to Dalamud's logs. This may be set + /// by either the plugin or by Dalamud itself. + /// + /// + /// Defaults to for downloaded plugins, and + /// for dev plugins. + /// + LogEventLevel MinimumLogLevel { get; set; } + + /// + /// Gets an instance of the Serilog for advanced use cases. The provided logger will handle + /// tagging all log messages with the appropriate context variables and properties. + /// + /// + /// Not currently part of public API - will be added after some formatter work has been completed. + /// + internal ILogger Logger { get; } + + /// + /// Log a message to the Dalamud log for this plugin. This log level should be + /// used primarily for unrecoverable errors or critical faults in a plugin. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Fatal(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Fatal(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log a message to the Dalamud log for this plugin. This log level should be + /// used for recoverable errors or faults that impact plugin functionality. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Error(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Error(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log a message to the Dalamud log for this plugin. This log level should be + /// used for user error, potential problems, or high-importance messages that should be logged. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Warning(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Warning(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log an message to the Dalamud log for this plugin. This log level + /// should be used for general plugin operations and other relevant information to track a plugin's behavior. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Information(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Information(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log a message to the Dalamud log for this plugin. This log level should be + /// used for messages or information that aid with debugging or tracing a plugin's operations, but should not be + /// recorded unless requested. + /// + /// + /// By default, this log level is below the default log level of Dalamud. Messages logged at this level will not be + /// recorded unless the global log level is specifically set to Debug or lower. If information should be generally + /// or easily accessible for support purposes without the user taking additional action, consider using the + /// Information level instead. Developers should not use this log level where it can be triggered on a + /// per-frame basis. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Debug(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Debug(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log a message to the Dalamud log for this plugin. This log level is + /// intended almost primarily for development purposes and detailed tracing of a plugin's operations. Verbose logs + /// should not be used to expose information useful for support purposes. + /// + /// + /// By default, this log level is below the default log level of Dalamud. Messages logged at this level will not be + /// recorded unless the global log level is specifically set to Verbose. Release plugins must also set the + /// to to use this level, and should only do so + /// upon specific user request (e.g. a "Enable Troubleshooting Logs" button). + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Verbose(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Verbose(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Write a raw log event to the plugin's log. Used for interoperability with other log systems, as well as + /// advanced use cases. + /// + /// The log level for this event. + /// An (optional) exception that should be recorded alongside this event. + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Write(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values); +} From 40b875c8e93b796eb9104233263bf6bd790afc6d Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:37:05 -0700 Subject: [PATCH 40/40] Remove inconsistent reference to `third-party` (#1372) --- .../Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs index 6e476792b..2fac28070 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs @@ -96,7 +96,7 @@ public class ThirdRepoSettingsEntry : SettingsEntry ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning5", "If someone told you to copy/paste something here, it's very possible that you are being scammed or taken advantage of.")); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning2", "Plugins have full control over your PC, like any other program, and may cause harm or crashes.")); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning4", "They can delete your character, steal your FC or Discord account, and burn down your house.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning3", "Please make absolutely sure that you only install third-party plugins from developers you trust.")); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning3", "Please make absolutely sure that you only install plugins from developers you trust.")); if (!disclaimerDismissed) {