From 0d20ff25e431ee53b52becd0057b513aea46e4a2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 5 Aug 2023 13:11:24 +0200 Subject: [PATCH 001/122] fix: missing ## for id in DisabledButton --- Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs index 181bbbfd7..907ad0aeb 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs @@ -25,7 +25,7 @@ public static partial class ImGuiComponents var text = icon.ToIconString(); if (id.HasValue) - text = $"{text}{id}"; + text = $"{text}##{id}"; var button = DisabledButton(text, defaultColor, activeColor, hoveredColor, alphaMult); From cd3d23e0fd6f1a477d245d86f7ccb82aa9db0530 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:16:41 +0200 Subject: [PATCH 002/122] Update ClientStructs (#1339) 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 782d73171..155511a61 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 782d7317176f232d7108b2b3a4cb75de67fc3a8a +Subproject commit 155511a61559c529719d3810eaf8fb9336482878 From b4c24305f23140194dc198355ec6e6060515ade6 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 18:42:23 +0200 Subject: [PATCH 003/122] chore: add pre-filled FTUE level to config, so that existing users will not get a FTUE once we add it --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 39c53c3cb..ac410527c 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -69,6 +69,12 @@ internal sealed class DalamudConfiguration : IServiceType /// public string LastVersion { get; set; } = null; + /// + /// Gets or sets a value indicating the last seen FTUE version. + /// Unused for now, added to prevent existing users from seeing level 0 FTUE. + /// + public int SeenFtueLevel { get; set; } = 1; + /// /// Gets or sets the last loaded Dalamud version. /// From 426e7f53efe999a05ec8a5211ef725a4357151da Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 18:42:52 +0200 Subject: [PATCH 004/122] build: 7.10.0.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 12068a35c..494c0f297 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.9.0.0 + 7.10.0.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From ebc4baca585abadcd326292d379a7ba0408537a1 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 21:36:46 +0200 Subject: [PATCH 005/122] 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 006/122] 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 007/122] 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 008/122] 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 009/122] 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 010/122] 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 011/122] 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 a6ed6dabe4206b445cad9b0f16d53633287c3bdf Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 10 Aug 2023 02:10:45 +0200 Subject: [PATCH 012/122] refactor: use IEnumerable instead of List in SeString.Append --- Dalamud/Game/Text/SeStringHandling/SeString.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 2ddb73f12..6132d0910 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -421,7 +421,7 @@ public class SeString /// /// The Payloads to append. /// This object. - public SeString Append(List payloads) + public SeString Append(IEnumerable payloads) { this.Payloads.AddRange(payloads); return this; From 54790711ddcd6604f108f31fbd93fd4c3ec15240 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 10 Aug 2023 02:11:59 +0200 Subject: [PATCH 013/122] refactor: append payload list directly in SeStringBuilder --- Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs index 1fda9f9ae..dae9e11a9 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs @@ -38,7 +38,11 @@ public class SeStringBuilder /// /// A list of payloads. /// The current builder. - public SeStringBuilder Append(IEnumerable payloads) => this.Append(new SeString(payloads.ToList())); + public SeStringBuilder Append(IEnumerable payloads) + { + this.BuiltString.Payloads.AddRange(payloads); + return this; + } /// /// Append raw text to the builder. From 4c15df80b9cf636d7e08c106292818e704d495f0 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 10 Aug 2023 19:11:48 +0200 Subject: [PATCH 014/122] 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 015/122] 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 016/122] 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 017/122] 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 018/122] 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 019/122] 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 020/122] 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 021/122] 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 022/122] 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 023/122] 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 024/122] 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 025/122] 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 026/122] 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 027/122] 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 028/122] 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 029/122] 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 030/122] 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 031/122] 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 032/122] 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 033/122] 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 034/122] 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 035/122] 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 342e1bc06c60b82bd077519058612eefa754dec3 Mon Sep 17 00:00:00 2001 From: srkizer Date: Tue, 29 Aug 2023 10:04:29 +0900 Subject: [PATCH 036/122] Make TextureManager.GetTexture return value non-nullable (#1342) --- 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 1648f1961..b9efb42f3 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -203,7 +203,9 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// /// The texture to obtain a handle to. /// A texture wrap that can be used to render the texture. - public IDalamudTextureWrap? GetTexture(TexFile file) + /// Thrown when the graphics system is not available yet. Relevant for plugins when LoadRequiredState is set to 0 or 1. + /// Thrown when the given is not supported. Most likely is that the file is corrupt. + public IDalamudTextureWrap GetTexture(TexFile file) { ArgumentNullException.ThrowIfNull(file); 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 037/122] 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 038/122] [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 039/122] [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 040/122] 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 c095f99cd1378db36a31972ac870f13b354ec9a9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 1 Sep 2023 21:53:34 -0700 Subject: [PATCH 041/122] Add AddonEventManager --- .../AddonEventManager/AddonEventManager.cs | 207 ++++++++++++++++++ .../AddonEventManagerAddressResolver.cs | 21 ++ .../Game/AddonEventManager/AddonEventType.cs | 132 +++++++++++ Dalamud/Plugin/Services/IAddonEventManager.cs | 36 +++ 4 files changed, 396 insertions(+) create mode 100644 Dalamud/Game/AddonEventManager/AddonEventManager.cs create mode 100644 Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs create mode 100644 Dalamud/Game/AddonEventManager/AddonEventType.cs create mode 100644 Dalamud/Plugin/Services/IAddonEventManager.cs diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs new file mode 100644 index 000000000..8a0f16d1e --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; + +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.AddonEventManager; + +/// +/// Service provider for addon event management. +/// +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal unsafe class AddonEventManager : IDisposable, IServiceType +{ + // The starting value for param key ranges. + // ascii `DD` 0x4444 chosen for the key start, this just has to be larger than anything vanilla makes. + private const uint ParamKeyStart = 0x44440000; + + // The range each plugin is allowed to use. + // 65,536 per plugin should be reasonable. + private const uint ParamKeyPluginRange = 0x10000; + + // The maximum range allowed to be given to a plugin. + // 20,560 maximum plugins should be reasonable. + // 202,113,024 maximum event handlers should be reasonable. + private const uint ParamKeyMax = 0x50500000; + + private static readonly ModuleLog Log = new("AddonEventManager"); + private readonly AddonEventManagerAddressResolver address; + private readonly Hook onGlobalEventHook; + private readonly Dictionary eventHandlers; + + private uint currentPluginParamStart = ParamKeyStart; + + [ServiceManager.ServiceConstructor] + private AddonEventManager(SigScanner sigScanner) + { + this.address = new AddonEventManagerAddressResolver(); + this.address.Setup(sigScanner); + + this.eventHandlers = new Dictionary(); + + this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler); + } + + private delegate nint GlobalEventHandlerDetour(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown); + + /// + public void Dispose() + { + this.onGlobalEventHook.Dispose(); + } + + /// + /// Get the start value for a new plugin register. + /// + /// A unique starting range for event handlers. + /// Throws when attempting to register too many event handlers. + public uint GetPluginParamStart() + { + if (this.currentPluginParamStart >= ParamKeyMax) + { + throw new Exception("Maximum number of event handlers reached."); + } + + var result = this.currentPluginParamStart; + + this.currentPluginParamStart += ParamKeyPluginRange; + return result; + } + + /// + /// Adds a event handler to be triggered when the specified id is received. + /// + /// Unique id for this event handler. + /// The event handler to be called. + public void AddEvent(uint eventId, IAddonEventManager.AddonEventHandler handler) => this.eventHandlers.Add(eventId, handler); + + /// + /// Removes the event handler with the specified id. + /// + /// Event id to unregister. + public void RemoveEvent(uint eventId) => this.eventHandlers.Remove(eventId); + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction() + { + this.onGlobalEventHook.Enable(); + } + + private nint GlobalEventHandler(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown) + { + try + { + if (this.eventHandlers.TryGetValue(eventParam, out var handler)) + { + try + { + handler?.Invoke((AddonEventType)eventType, (nint)atkUnitBase, (nint)eventData[0]); + return nint.Zero; + } + catch (Exception exception) + { + Log.Error(exception, "Exception in GlobalEventHandler custom event invoke."); + } + } + } + catch (Exception e) + { + Log.Error(e, "Exception in GlobalEventHandler attempting to retrieve event handler."); + } + + return this.onGlobalEventHook!.Original(atkUnitBase, eventType, eventParam, eventData, unknown); + } +} + +/// +/// Plugin-scoped version of a AddonEventManager service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager +{ + private static readonly ModuleLog Log = new("AddonEventManager"); + + [ServiceManager.ServiceDependency] + private readonly AddonEventManager baseEventManager = Service.Get(); + + private readonly uint paramKeyStartRange; + private readonly List activeParamKeys; + + /// + /// Initializes a new instance of the class. + /// + public AddonEventManagerPluginScoped() + { + this.paramKeyStartRange = this.baseEventManager.GetPluginParamStart(); + this.activeParamKeys = new List(); + } + + /// + public void Dispose() + { + foreach (var activeKey in this.activeParamKeys) + { + this.baseEventManager.RemoveEvent(activeKey); + } + } + + /// + public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + { + if (eventId < 0x10000) + { + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + var eventListener = (AtkEventListener*)atkUnitBase; + var uniqueId = eventId + this.paramKeyStartRange; + + if (!this.activeParamKeys.Contains(uniqueId)) + { + node->AddEvent(type, uniqueId, eventListener, node, true); + this.baseEventManager.AddEvent(uniqueId, eventHandler); + + this.activeParamKeys.Add(uniqueId); + } + else + { + Log.Warning($"Attempted to register already registered eventId: {eventId}"); + } + } + else + { + Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10000}"); + } + } + + /// + public void RemoveEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType) + { + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + var eventListener = (AtkEventListener*)atkUnitBase; + var uniqueId = eventId + this.paramKeyStartRange; + + if (this.activeParamKeys.Contains(uniqueId)) + { + node->RemoveEvent(type, uniqueId, eventListener, true); + this.baseEventManager.RemoveEvent(uniqueId); + + this.activeParamKeys.Remove(uniqueId); + } + else + { + Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); + } + } +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs new file mode 100644 index 000000000..8dcf81580 --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs @@ -0,0 +1,21 @@ +namespace Dalamud.Game.AddonEventManager; + +/// +/// AddonEventManager memory address resolver. +/// +internal class AddonEventManagerAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the global atkevent handler + /// + public nint GlobalEventHandler { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(SigScanner scanner) + { + this.GlobalEventHandler = scanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2"); + } +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventType.cs b/Dalamud/Game/AddonEventManager/AddonEventType.cs new file mode 100644 index 000000000..eef9763ff --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventType.cs @@ -0,0 +1,132 @@ +namespace Dalamud.Game.AddonEventManager; + +/// +/// Reimplementation of AtkEventType. +/// +public enum AddonEventType : byte +{ + /// + /// Mouse Down. + /// + MouseDown = 3, + + /// + /// Mouse Up. + /// + MouseUp = 4, + + /// + /// Mouse Move. + /// + MouseMove = 5, + + /// + /// Mouse Over. + /// + MouseOver = 6, + + /// + /// Mouse Out. + /// + MouseOut = 7, + + /// + /// Mouse Click. + /// + MouseClick = 9, + + /// + /// Input Received. + /// + InputReceived = 12, + + /// + /// Focus Start. + /// + FocusStart = 18, + + /// + /// Focus Stop. + /// + FocusStop = 19, + + /// + /// Button Press, sent on MouseDown on Button. + /// + ButtonPress = 23, + + /// + /// Button Release, sent on MouseUp and MouseOut. + /// + ButtonRelease = 24, + + /// + /// Button Click, sent on MouseUp and MouseClick on button. + /// + ButtonClick = 25, + + /// + /// List Item RollOver. + /// + ListItemRollOver = 33, + + /// + /// List Item Roll Out. + /// + ListItemRollOut = 34, + + /// + /// List Item Toggle. + /// + ListItemToggle = 35, + + /// + /// Drag Drop Roll Over. + /// + DragDropRollOver = 52, + + /// + /// Drag Drop Roll Out. + /// + DragDropRollOut = 53, + + /// + /// Drag Drop Unknown. + /// + DragDropUnk54 = 54, + + /// + /// Drag Drop Unknown. + /// + DragDropUnk55 = 55, + + /// + /// Icon Text Roll Over. + /// + IconTextRollOver = 56, + + /// + /// Icon Text Roll Out. + /// + IconTextRollOut = 57, + + /// + /// Icon Text Click. + /// + IconTextClick = 58, + + /// + /// Window Roll Over. + /// + WindowRollOver = 67, + + /// + /// Window Roll Out. + /// + WindowRollOut = 68, + + /// + /// Window Change Scale. + /// + WindowChangeScale = 69, +} diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs new file mode 100644 index 000000000..8e2b1f67b --- /dev/null +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -0,0 +1,36 @@ +using Dalamud.Game.AddonEventManager; + +namespace Dalamud.Plugin.Services; + +/// +/// Service provider for addon event management. +/// +public interface IAddonEventManager +{ + /// + /// Delegate to be called when an event is received. + /// + /// Event type for this event handler. + /// The parent addon for this event handler. + /// The specific node that will trigger this event handler. + public delegate void AddonEventHandler(AddonEventType atkEventType, nint atkUnitBase, nint atkResNode); + + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// Unique Id for this event, maximum 0x10000. + /// The parent addon for this event. + /// The node that will trigger this event. + /// The event type for this event. + /// The handler to call when event is triggered. + void AddEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler); + + /// + /// Unregisters an event handler with the specified event id and event type. + /// + /// The Unique Id for this event. + /// The parent addon for this event. + /// The node for this event. + /// The event type for this event. + void RemoveEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType); +} From 7ac37a579b740541fc36b7f9d4131634c823b1ea Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 1 Sep 2023 23:33:10 -0700 Subject: [PATCH 042/122] [AddonEventManager] Add null check --- Dalamud/Game/AddonEventManager/AddonEventManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 8a0f16d1e..77111421f 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -97,7 +97,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType { try { - if (this.eventHandlers.TryGetValue(eventParam, out var handler)) + if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) { try { From ad06b5f05486d92be0d2ae28f95c05244159bd84 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 00:06:54 -0700 Subject: [PATCH 043/122] [AddonEventManager] Add Cursor Control --- .../Game/AddonEventManager/AddonCursorType.cs | 97 +++++++++++++++++++ .../AddonEventManager/AddonEventManager.cs | 84 +++++++++++++++- .../AddonEventManagerAddressResolver.cs | 8 +- Dalamud/Plugin/Services/IAddonEventManager.cs | 11 +++ 4 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 Dalamud/Game/AddonEventManager/AddonCursorType.cs diff --git a/Dalamud/Game/AddonEventManager/AddonCursorType.cs b/Dalamud/Game/AddonEventManager/AddonCursorType.cs new file mode 100644 index 000000000..8ba3a901b --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonCursorType.cs @@ -0,0 +1,97 @@ +namespace Dalamud.Game.AddonEventManager; + +/// +/// Reimplementation of CursorType. +/// +public enum AddonCursorType +{ + /// + /// Arrow. + /// + Arrow, + + /// + /// Boot. + /// + Boot, + + /// + /// Search. + /// + Search, + + /// + /// Chat Pointer. + /// + ChatPointer, + + /// + /// Interact. + /// + Interact, + + /// + /// Attack. + /// + Attack, + + /// + /// Hand. + /// + Hand, + + /// + /// Resizeable Left-Right. + /// + ResizeWE, + + /// + /// Resizeable Up-Down. + /// + ResizeNS, + + /// + /// Resizeable. + /// + ResizeNWSR, + + /// + /// Resizeable 4-way. + /// + ResizeNESW, + + /// + /// Clickable. + /// + Clickable, + + /// + /// Text Input. + /// + TextInput, + + /// + /// Text Click. + /// + TextClick, + + /// + /// Grab. + /// + Grab, + + /// + /// Chat Bubble. + /// + ChatBubble, + + /// + /// No Access. + /// + NoAccess, + + /// + /// Hidden. + /// + Hidden, +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 77111421f..ede59a9a9 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -6,6 +6,7 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.AddonEventManager; @@ -32,9 +33,13 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType private static readonly ModuleLog Log = new("AddonEventManager"); private readonly AddonEventManagerAddressResolver address; - private readonly Hook onGlobalEventHook; + private readonly Hook onGlobalEventHook; + private readonly Hook onUpdateCursor; private readonly Dictionary eventHandlers; + private AddonCursorType currentCursor; + private bool cursorSet; + private uint currentPluginParamStart = ParamKeyStart; [ServiceManager.ServiceConstructor] @@ -44,16 +49,21 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address.Setup(sigScanner); this.eventHandlers = new Dictionary(); + this.currentCursor = AddonCursorType.Arrow; - this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler); + this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler); + this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); } - private delegate nint GlobalEventHandlerDetour(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown); + private delegate nint GlobalEventHandlerDelegate(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown); + + private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); /// public void Dispose() { this.onGlobalEventHook.Dispose(); + this.onUpdateCursor.Dispose(); } /// @@ -86,11 +96,31 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// /// Event id to unregister. public void RemoveEvent(uint eventId) => this.eventHandlers.Remove(eventId); + + /// + /// Sets the game cursor. + /// + /// Cursor type to set. + public void SetCursor(AddonCursorType cursor) + { + this.currentCursor = cursor; + this.cursorSet = true; + } + + /// + /// Resets and un-forces custom cursor. + /// + public void ResetCursor() + { + this.currentCursor = AddonCursorType.Arrow; + this.cursorSet = false; + } [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { this.onGlobalEventHook.Enable(); + this.onUpdateCursor.Enable(); } private nint GlobalEventHandler(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown) @@ -117,6 +147,31 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType return this.onGlobalEventHook!.Original(atkUnitBase, eventType, eventParam, eventData, unknown); } + + private nint UpdateCursorDetour(RaptureAtkModule* module) + { + try + { + var atkStage = AtkStage.GetSingleton(); + + if (this.cursorSet && atkStage is not null) + { + var cursor = (AddonCursorType)atkStage->AtkCursor.Type; + if (cursor != this.currentCursor) + { + AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.currentCursor, 1); + } + + return nint.Zero; + } + } + catch (Exception e) + { + Log.Error(e, "Exception in UpdateCursorDetour."); + } + + return this.onUpdateCursor!.Original(module); + } } /// @@ -137,6 +192,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, private readonly uint paramKeyStartRange; private readonly List activeParamKeys; + private bool isForcingCursor; /// /// Initializes a new instance of the class. @@ -154,6 +210,12 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, { this.baseEventManager.RemoveEvent(activeKey); } + + // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. + if (this.isForcingCursor) + { + this.baseEventManager.ResetCursor(); + } } /// @@ -204,4 +266,20 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); } } + + /// + public void SetCursor(AddonCursorType cursor) + { + this.isForcingCursor = true; + + this.baseEventManager.SetCursor(cursor); + } + + /// + public void ResetCursor() + { + this.isForcingCursor = false; + + this.baseEventManager.ResetCursor(); + } } diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs index 8dcf81580..5cfa51149 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs @@ -6,9 +6,14 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver { /// - /// Gets the address of the global atkevent handler + /// Gets the address of the global AtkEvent handler. /// public nint GlobalEventHandler { get; private set; } + + /// + /// Gets the address of the AtkModule UpdateCursor method. + /// + public nint UpdateCursor { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -17,5 +22,6 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver protected override void Setup64Bit(SigScanner scanner) { this.GlobalEventHandler = scanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2"); + this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); } } diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index 8e2b1f67b..f052ed607 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -33,4 +33,15 @@ public interface IAddonEventManager /// The node for this event. /// The event type for this event. void RemoveEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType); + + /// + /// Force the game cursor to be the specified cursor. + /// + /// Which cursor to use. + void SetCursor(AddonCursorType cursor); + + /// + /// Un-forces the game cursor. + /// + void ResetCursor(); } From 712a492c89b919ad38c4f293da0308a974d74e54 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 09:41:21 -0700 Subject: [PATCH 044/122] [AddonEventController] Use custom AtkEventListener --- .../AddonEventManager/AddonEventListener.cs | 87 +++++++++++++++ .../AddonEventManager/AddonEventManager.cs | 103 +++++++++--------- .../AddonEventManagerAddressResolver.cs | 6 - 3 files changed, 141 insertions(+), 55 deletions(-) create mode 100644 Dalamud/Game/AddonEventManager/AddonEventListener.cs diff --git a/Dalamud/Game/AddonEventManager/AddonEventListener.cs b/Dalamud/Game/AddonEventManager/AddonEventListener.cs new file mode 100644 index 000000000..cb0aa1502 --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventListener.cs @@ -0,0 +1,87 @@ +using System; +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonEventManager; + +/// +/// Event listener class for managing custom events. +/// +// Custom event handler tech provided by Pohky, implemented by MidoriKami +internal unsafe class AddonEventListener : IDisposable +{ + private ReceiveEventDelegate? receiveEventDelegate; + + private AtkEventListener* eventListener; + + /// + /// Initializes a new instance of the class. + /// + /// The managed handler to send events to. + public AddonEventListener(ReceiveEventDelegate eventHandler) + { + this.receiveEventDelegate = eventHandler; + + this.eventListener = (AtkEventListener*)Marshal.AllocHGlobal(sizeof(AtkEventListener)); + this.eventListener->vtbl = (void*)Marshal.AllocHGlobal(sizeof(void*) * 3); + this.eventListener->vfunc[0] = (delegate* unmanaged)&NullSub; + this.eventListener->vfunc[1] = (delegate* unmanaged)&NullSub; + this.eventListener->vfunc[2] = (void*)Marshal.GetFunctionPointerForDelegate(this.receiveEventDelegate); + } + + /// + /// Delegate for receiving custom events. + /// + /// Pointer to the event listener. + /// Event type. + /// Unique Id for this event. + /// Event Data. + /// Unknown Parameter. + public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown); + + /// + public void Dispose() + { + if (this.eventListener is null) return; + + Marshal.FreeHGlobal((nint)this.eventListener->vtbl); + Marshal.FreeHGlobal((nint)this.eventListener); + + this.eventListener = null; + this.receiveEventDelegate = null; + } + + /// + /// Register an event to this event handler. + /// + /// Addon that triggers this event. + /// Node to attach event to. + /// Event type to trigger this event. + /// Unique id for this event. + public void RegisterEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param) + { + if (node is null) return; + + node->AddEvent(eventType, param, this.eventListener, (AtkResNode*)addon, false); + } + + /// + /// Unregister an event from this event handler. + /// + /// Node to remove the event from. + /// Event type that this event is for. + /// Unique id for this event. + public void UnregisterEvent(AtkResNode* node, AtkEventType eventType, uint param) + { + if (node is null) return; + + node->RemoveEvent(eventType, param, this.eventListener, false); + } + + [UnmanagedCallersOnly] + private static void NullSub() + { + /* do nothing */ + } +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index ede59a9a9..c6e61fe5a 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -19,23 +19,22 @@ namespace Dalamud.Game.AddonEventManager; internal unsafe class AddonEventManager : IDisposable, IServiceType { // The starting value for param key ranges. - // ascii `DD` 0x4444 chosen for the key start, this just has to be larger than anything vanilla makes. - private const uint ParamKeyStart = 0x44440000; + private const uint ParamKeyStart = 0x0000_0000; // The range each plugin is allowed to use. - // 65,536 per plugin should be reasonable. - private const uint ParamKeyPluginRange = 0x10000; + // 1,048,576 per plugin should be reasonable. + private const uint ParamKeyPluginRange = 0x10_0000; // The maximum range allowed to be given to a plugin. - // 20,560 maximum plugins should be reasonable. - // 202,113,024 maximum event handlers should be reasonable. - private const uint ParamKeyMax = 0x50500000; + // 1,048,576 maximum plugins should be reasonable. + private const uint ParamKeyMax = 0xFFF0_0000; private static readonly ModuleLog Log = new("AddonEventManager"); + private readonly AddonEventManagerAddressResolver address; - private readonly Hook onGlobalEventHook; private readonly Hook onUpdateCursor; private readonly Dictionary eventHandlers; + private readonly AddonEventListener eventListener; private AddonCursorType currentCursor; private bool cursorSet; @@ -48,21 +47,20 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); + this.eventListener = new AddonEventListener(this.OnCustomEvent); + this.eventHandlers = new Dictionary(); this.currentCursor = AddonCursorType.Arrow; - this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler); this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); } - private delegate nint GlobalEventHandlerDelegate(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown); - private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); /// public void Dispose() { - this.onGlobalEventHook.Dispose(); + this.eventListener.Dispose(); this.onUpdateCursor.Dispose(); } @@ -85,17 +83,39 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType } /// - /// Adds a event handler to be triggered when the specified id is received. + /// Attaches an event to a node. /// - /// Unique id for this event handler. + /// Addon that contains the node. + /// The node that will trigger the event. + /// The event type to trigger on. + /// The unique id for this event. /// The event handler to be called. - public void AddEvent(uint eventId, IAddonEventManager.AddonEventHandler handler) => this.eventHandlers.Add(eventId, handler); + public void AddEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param, IAddonEventManager.AddonEventHandler handler) + { + this.eventListener.RegisterEvent(addon, node, eventType, param); + this.eventHandlers.TryAdd(param, handler); + } /// - /// Removes the event handler with the specified id. + /// Detaches an event from a node. /// - /// Event id to unregister. - public void RemoveEvent(uint eventId) => this.eventHandlers.Remove(eventId); + /// The node to remove the event from. + /// The event type to remove. + /// The unique id of the event to remove. + public void RemoveEvent(AtkResNode* node, AtkEventType eventType, uint param) + { + this.eventListener.UnregisterEvent(node, eventType, param); + this.eventHandlers.Remove(param); + } + + /// + /// Removes a delegate from the managed event handlers. + /// + /// Unique id of the delegate to remove. + public void RemoveHandler(uint param) + { + this.eventHandlers.Remove(param); + } /// /// Sets the game cursor. @@ -119,33 +139,23 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { - this.onGlobalEventHook.Enable(); this.onUpdateCursor.Enable(); } - private nint GlobalEventHandler(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown) + private void OnCustomEvent(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown) { - try + if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) { - if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) + try { - try - { - handler?.Invoke((AddonEventType)eventType, (nint)atkUnitBase, (nint)eventData[0]); - return nint.Zero; - } - catch (Exception exception) - { - Log.Error(exception, "Exception in GlobalEventHandler custom event invoke."); - } + // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler + handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); + } + catch (Exception exception) + { + Log.Error(exception, "Exception in OnCustomEvent custom event invoke."); } } - catch (Exception e) - { - Log.Error(e, "Exception in GlobalEventHandler attempting to retrieve event handler."); - } - - return this.onGlobalEventHook!.Original(atkUnitBase, eventType, eventParam, eventData, unknown); } private nint UpdateCursorDetour(RaptureAtkModule* module) @@ -193,7 +203,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, private readonly uint paramKeyStartRange; private readonly List activeParamKeys; private bool isForcingCursor; - + /// /// Initializes a new instance of the class. /// @@ -208,7 +218,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, { foreach (var activeKey in this.activeParamKeys) { - this.baseEventManager.RemoveEvent(activeKey); + this.baseEventManager.RemoveHandler(activeKey); } // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. @@ -221,18 +231,16 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, /// public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) { - if (eventId < 0x10000) + if (eventId < 0x10_000) { var type = (AtkEventType)eventType; var node = (AtkResNode*)atkResNode; - var eventListener = (AtkEventListener*)atkUnitBase; + var addon = (AtkUnitBase*)atkUnitBase; var uniqueId = eventId + this.paramKeyStartRange; if (!this.activeParamKeys.Contains(uniqueId)) { - node->AddEvent(type, uniqueId, eventListener, node, true); - this.baseEventManager.AddEvent(uniqueId, eventHandler); - + this.baseEventManager.AddEvent(addon, node, type, uniqueId, eventHandler); this.activeParamKeys.Add(uniqueId); } else @@ -242,7 +250,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, } else { - Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10000}"); + Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10_000}"); } } @@ -251,14 +259,11 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, { var type = (AtkEventType)eventType; var node = (AtkResNode*)atkResNode; - var eventListener = (AtkEventListener*)atkUnitBase; var uniqueId = eventId + this.paramKeyStartRange; if (this.activeParamKeys.Contains(uniqueId)) { - node->RemoveEvent(type, uniqueId, eventListener, true); - this.baseEventManager.RemoveEvent(uniqueId); - + this.baseEventManager.RemoveEvent(node, type, uniqueId); this.activeParamKeys.Remove(uniqueId); } else diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs index 5cfa51149..ba1c07db8 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs @@ -5,11 +5,6 @@ /// internal class AddonEventManagerAddressResolver : BaseAddressResolver { - /// - /// Gets the address of the global AtkEvent handler. - /// - public nint GlobalEventHandler { get; private set; } - /// /// Gets the address of the AtkModule UpdateCursor method. /// @@ -21,7 +16,6 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver /// The signature scanner to facilitate setup. protected override void Setup64Bit(SigScanner scanner) { - this.GlobalEventHandler = scanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2"); this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); } } From 22a381b8746bfc58d14a84a815eabd030ac03f12 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 09:49:21 -0700 Subject: [PATCH 045/122] [AddonEventManager] Reserve the first range for Dalamud internal use --- Dalamud/Game/AddonEventManager/AddonEventManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index c6e61fe5a..b88c64253 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -19,7 +19,8 @@ namespace Dalamud.Game.AddonEventManager; internal unsafe class AddonEventManager : IDisposable, IServiceType { // The starting value for param key ranges. - private const uint ParamKeyStart = 0x0000_0000; + // Reserve the first 0x10_000 for dalamud internal use. + private const uint ParamKeyStart = 0x0010_0000; // The range each plugin is allowed to use. // 1,048,576 per plugin should be reasonable. From 26e138c7834011f385fa9c0bd4cd66fbad55f247 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 11:30:31 -0700 Subject: [PATCH 046/122] [AddonEventManager] Give each plugin their own event listener. --- .../AddonEventManager/AddonEventManager.cs | 180 +++++------------- 1 file changed, 44 insertions(+), 136 deletions(-) diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index b88c64253..69dc27f3b 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -18,29 +18,12 @@ namespace Dalamud.Game.AddonEventManager; [ServiceManager.EarlyLoadedService] internal unsafe class AddonEventManager : IDisposable, IServiceType { - // The starting value for param key ranges. - // Reserve the first 0x10_000 for dalamud internal use. - private const uint ParamKeyStart = 0x0010_0000; - - // The range each plugin is allowed to use. - // 1,048,576 per plugin should be reasonable. - private const uint ParamKeyPluginRange = 0x10_0000; - - // The maximum range allowed to be given to a plugin. - // 1,048,576 maximum plugins should be reasonable. - private const uint ParamKeyMax = 0xFFF0_0000; - private static readonly ModuleLog Log = new("AddonEventManager"); private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; - private readonly Dictionary eventHandlers; - private readonly AddonEventListener eventListener; - private AddonCursorType currentCursor; - private bool cursorSet; - - private uint currentPluginParamStart = ParamKeyStart; + private AddonCursorType? cursorOverride; [ServiceManager.ServiceConstructor] private AddonEventManager(SigScanner sigScanner) @@ -48,10 +31,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); - this.eventListener = new AddonEventListener(this.OnCustomEvent); - - this.eventHandlers = new Dictionary(); - this.currentCursor = AddonCursorType.Arrow; + this.cursorOverride = null; this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); } @@ -61,116 +41,38 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// public void Dispose() { - this.eventListener.Dispose(); this.onUpdateCursor.Dispose(); } - /// - /// Get the start value for a new plugin register. - /// - /// A unique starting range for event handlers. - /// Throws when attempting to register too many event handlers. - public uint GetPluginParamStart() - { - if (this.currentPluginParamStart >= ParamKeyMax) - { - throw new Exception("Maximum number of event handlers reached."); - } - - var result = this.currentPluginParamStart; - - this.currentPluginParamStart += ParamKeyPluginRange; - return result; - } - - /// - /// Attaches an event to a node. - /// - /// Addon that contains the node. - /// The node that will trigger the event. - /// The event type to trigger on. - /// The unique id for this event. - /// The event handler to be called. - public void AddEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param, IAddonEventManager.AddonEventHandler handler) - { - this.eventListener.RegisterEvent(addon, node, eventType, param); - this.eventHandlers.TryAdd(param, handler); - } - - /// - /// Detaches an event from a node. - /// - /// The node to remove the event from. - /// The event type to remove. - /// The unique id of the event to remove. - public void RemoveEvent(AtkResNode* node, AtkEventType eventType, uint param) - { - this.eventListener.UnregisterEvent(node, eventType, param); - this.eventHandlers.Remove(param); - } - - /// - /// Removes a delegate from the managed event handlers. - /// - /// Unique id of the delegate to remove. - public void RemoveHandler(uint param) - { - this.eventHandlers.Remove(param); - } - /// /// Sets the game cursor. /// /// Cursor type to set. - public void SetCursor(AddonCursorType cursor) - { - this.currentCursor = cursor; - this.cursorSet = true; - } + public void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; /// /// Resets and un-forces custom cursor. /// - public void ResetCursor() - { - this.currentCursor = AddonCursorType.Arrow; - this.cursorSet = false; - } - + public void ResetCursor() => this.cursorOverride = null; + [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { this.onUpdateCursor.Enable(); } - private void OnCustomEvent(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown) - { - if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) - { - try - { - // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler - handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); - } - catch (Exception exception) - { - Log.Error(exception, "Exception in OnCustomEvent custom event invoke."); - } - } - } - private nint UpdateCursorDetour(RaptureAtkModule* module) { try { var atkStage = AtkStage.GetSingleton(); - if (this.cursorSet && atkStage is not null) + if (this.cursorOverride is not null && atkStage is not null) { var cursor = (AddonCursorType)atkStage->AtkCursor.Type; - if (cursor != this.currentCursor) + if (cursor != this.cursorOverride) { - AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.currentCursor, 1); + AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); } return nint.Zero; @@ -200,9 +102,10 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, [ServiceManager.ServiceDependency] private readonly AddonEventManager baseEventManager = Service.Get(); - - private readonly uint paramKeyStartRange; - private readonly List activeParamKeys; + + private readonly AddonEventListener eventListener; + private readonly Dictionary eventHandlers; + private bool isForcingCursor; /// @@ -210,62 +113,51 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, /// public AddonEventManagerPluginScoped() { - this.paramKeyStartRange = this.baseEventManager.GetPluginParamStart(); - this.activeParamKeys = new List(); + this.eventHandlers = new Dictionary(); + this.eventListener = new AddonEventListener(this.PluginAddonEventHandler); } - + /// public void Dispose() { - foreach (var activeKey in this.activeParamKeys) - { - this.baseEventManager.RemoveHandler(activeKey); - } - // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. if (this.isForcingCursor) { this.baseEventManager.ResetCursor(); } + + this.eventListener.Dispose(); + this.eventHandlers.Clear(); } /// public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) { - if (eventId < 0x10_000) + if (!this.eventHandlers.ContainsKey(eventId)) { var type = (AtkEventType)eventType; var node = (AtkResNode*)atkResNode; var addon = (AtkUnitBase*)atkUnitBase; - var uniqueId = eventId + this.paramKeyStartRange; - if (!this.activeParamKeys.Contains(uniqueId)) - { - this.baseEventManager.AddEvent(addon, node, type, uniqueId, eventHandler); - this.activeParamKeys.Add(uniqueId); - } - else - { - Log.Warning($"Attempted to register already registered eventId: {eventId}"); - } + this.eventHandlers.Add(eventId, eventHandler); + this.eventListener.RegisterEvent(addon, node, type, eventId); } else { - Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10_000}"); + Log.Warning($"Attempted to register already registered eventId: {eventId}"); } } /// public void RemoveEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType) { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - var uniqueId = eventId + this.paramKeyStartRange; - - if (this.activeParamKeys.Contains(uniqueId)) + if (this.eventHandlers.ContainsKey(eventId)) { - this.baseEventManager.RemoveEvent(node, type, uniqueId); - this.activeParamKeys.Remove(uniqueId); + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + + this.eventListener.UnregisterEvent(node, type, eventId); + this.eventHandlers.Remove(eventId); } else { @@ -288,4 +180,20 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, this.baseEventManager.ResetCursor(); } + + private void PluginAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) + { + if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) + { + try + { + // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler + handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); + } + catch (Exception exception) + { + Log.Error(exception, "Exception in PluginAddonEventHandler custom event invoke."); + } + } + } } From 627a41f2363ee44956cd891950b8953cc1ff69c5 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:08:14 -0700 Subject: [PATCH 047/122] [AddonEventManager] Remove AtkUnitBase from remove event, it's not used to unregister events. --- Dalamud/Plugin/Services/IAddonEventManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index f052ed607..dbbfd784b 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -29,10 +29,9 @@ public interface IAddonEventManager /// Unregisters an event handler with the specified event id and event type. /// /// The Unique Id for this event. - /// The parent addon for this event. /// The node for this event. /// The event type for this event. - void RemoveEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType); + void RemoveEvent(uint eventId, nint atkResNode, AddonEventType eventType); /// /// Force the game cursor to be the specified cursor. From ce4392e1093557aebeb5dfeb0a8c4c0148e18b78 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:56:44 -0700 Subject: [PATCH 048/122] [AddonEventManager] Add Dalamud specific EventListener for internal use --- .../AddonEventManager/AddonEventManager.cs | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 69dc27f3b..4718d4800 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -16,13 +16,16 @@ namespace Dalamud.Game.AddonEventManager; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonEventManager : IDisposable, IServiceType +internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEventManager { private static readonly ModuleLog Log = new("AddonEventManager"); private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; + private readonly AddonEventListener eventListener; + private readonly Dictionary eventHandlers; + private AddonCursorType? cursorOverride; [ServiceManager.ServiceConstructor] @@ -31,28 +34,63 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); + this.eventHandlers = new Dictionary(); + this.eventListener = new AddonEventListener(this.DalamudAddonEventHandler); + this.cursorOverride = null; this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); } private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); + + /// + public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + { + if (!this.eventHandlers.ContainsKey(eventId)) + { + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + var addon = (AtkUnitBase*)atkUnitBase; + + this.eventHandlers.Add(eventId, eventHandler); + this.eventListener.RegisterEvent(addon, node, type, eventId); + } + else + { + Log.Warning($"Attempted to register already registered eventId: {eventId}"); + } + } + /// + public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType) + { + if (this.eventHandlers.ContainsKey(eventId)) + { + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + + this.eventListener.UnregisterEvent(node, type, eventId); + this.eventHandlers.Remove(eventId); + } + else + { + Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); + } + } + /// public void Dispose() { this.onUpdateCursor.Dispose(); + this.eventListener.Dispose(); + this.eventHandlers.Clear(); } - /// - /// Sets the game cursor. - /// - /// Cursor type to set. + /// public void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; - /// - /// Resets and un-forces custom cursor. - /// + /// public void ResetCursor() => this.cursorOverride = null; [ServiceManager.CallWhenServicesReady] @@ -85,6 +123,22 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType return this.onUpdateCursor!.Original(module); } + + private void DalamudAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) + { + if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) + { + try + { + // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler + handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); + } + catch (Exception exception) + { + Log.Error(exception, "Exception in DalamudAddonEventHandler custom event invoke."); + } + } + } } /// @@ -149,7 +203,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, } /// - public void RemoveEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType) + public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType) { if (this.eventHandlers.ContainsKey(eventId)) { From 2439bcccbd0b202d079290abd2ed0df8af7d7b09 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 4 Sep 2023 23:45:17 -0700 Subject: [PATCH 049/122] Add Tooltips, and OnClick actions to DtrBarEntries --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 209 +++++++++++++++--- Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs | 29 +++ Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 10 + 3 files changed, 218 insertions(+), 30 deletions(-) create mode 100644 Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index dd1e7aa30..4d2a005ae 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -3,13 +3,19 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; +using Dalamud.Game.AddonEventManager; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Memory; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; -using Serilog; + +using DalamudAddonEventManager = Dalamud.Game.AddonEventManager.AddonEventManager; namespace Dalamud.Game.Gui.Dtr; @@ -25,7 +31,12 @@ namespace Dalamud.Game.Gui.Dtr; public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; + private const uint MouseOverEventIdOffset = 10000; + private const uint MouseOutEventIdOffset = 20000; + private const uint MouseClickEventIdOffset = 30000; + private static readonly ModuleLog Log = new("DtrBar"); + [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -35,12 +46,24 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - private List entries = new(); + [ServiceManager.ServiceDependency] + private readonly DalamudAddonEventManager uiEventManager = Service.Get(); + + private readonly DtrBarAddressResolver address; + private readonly List entries = new(); + private readonly Hook onAddonDrawHook; + private readonly Hook onAddonRequestedUpdateHook; private uint runningNodeIds = BaseNodeId; [ServiceManager.ServiceConstructor] - private DtrBar() + private DtrBar(SigScanner sigScanner) { + this.address = new DtrBarAddressResolver(); + this.address.Setup(sigScanner); + + this.onAddonDrawHook = Hook.FromAddress(this.address.AtkUnitBaseDraw, this.OnAddonDrawDetour); + this.onAddonRequestedUpdateHook = Hook.FromAddress(this.address.AddonRequestedUpdate, this.OnAddonRequestedUpdateDetour); + this.framework.Update += this.Update; this.configuration.DtrOrder ??= new List(); @@ -48,6 +71,10 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.configuration.QueueSave(); } + private delegate void AddonDrawDelegate(AtkUnitBase* addon); + + private delegate void AddonRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + /// public DtrBarEntry Get(string title, SeString? text = null) { @@ -70,6 +97,9 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar /// void IDisposable.Dispose() { + this.onAddonDrawHook.Dispose(); + this.onAddonRequestedUpdateHook.Dispose(); + foreach (var entry in this.entries) this.RemoveNode(entry.TextNode); @@ -130,6 +160,13 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar return xPos.CompareTo(yPos); }); } + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction() + { + this.onAddonDrawHook.Enable(); + this.onAddonRequestedUpdateHook.Enable(); + } private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); @@ -148,7 +185,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (!this.CheckForDalamudNodes()) this.RecreateNodes(); - var collisionNode = dtr->UldManager.NodeList[1]; + var collisionNode = dtr->GetNodeById(17); if (collisionNode == null) return; // If we are drawing backwards, we should start from the right side of the collision node. That is, @@ -157,28 +194,24 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar ? collisionNode->X + collisionNode->Width : collisionNode->X; - for (var i = 0; i < this.entries.Count; i++) + foreach (var data in this.entries) { - var data = this.entries[i]; var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown; - if (data.Dirty && data.Added && data.Text != null && data.TextNode != null) + if (data is { Dirty: true, Added: true, Text: not null, TextNode: not null }) { var node = data.TextNode; - node->SetText(data.Text?.Encode()); + node->SetText(data.Text.Encode()); ushort w = 0, h = 0; - if (isHide) + if (!isHide) { - node->AtkResNode.ToggleVisibility(false); - } - else - { - node->AtkResNode.ToggleVisibility(true); node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); node->AtkResNode.SetWidth(w); } + node->AtkResNode.ToggleVisibility(!isHide); + data.Dirty = false; } @@ -202,8 +235,62 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); } } + } + } - this.entries[i] = data; + // This hooks all AtkUnitBase.Draw calls, then checks for our specific addon name. + // AddonDtr doesn't implement it's own Draw method, would need to replace vtable entry to be more efficient. + private void OnAddonDrawDetour(AtkUnitBase* addon) + { + this.onAddonDrawHook!.Original(addon); + + try + { + if (MemoryHelper.ReadString((nint)addon->Name, 0x20) is not "_DTR") return; + + this.UpdateNodePositions(addon); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonDraw."); + } + } + + private void UpdateNodePositions(AtkUnitBase* addon) + { + var targetSize = (ushort)this.CalculateTotalSize(); + addon->RootNode->SetWidth(targetSize); + + // If we grow to the right, we need to left-justify the original elements. + // else if we grow to the left, the game right-justifies it for us. + if (this.configuration.DtrSwapDirection) + { + var sizeOffset = addon->GetNodeById(17)->GetX(); + + var node = addon->RootNode->ChildNode; + while (node is not null) + { + if (node->NodeID < 1000 && node->IsVisible) + { + node->SetX(node->GetX() - sizeOffset); + } + + node = node->PrevSiblingNode; + } + } + } + + private void OnAddonRequestedUpdateDetour(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + this.onAddonRequestedUpdateHook.Original(addon, numberArrayData, stringArrayData); + + try + { + this.UpdateNodePositions(addon); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRequestedUpdate."); } } @@ -235,11 +322,37 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } + // Calculates the total width the dtr bar should be + private float CalculateTotalSize() + { + var addon = this.GetDtr(); + if (addon is null || addon->RootNode is null || addon->UldManager.NodeList is null) return 0; + + var totalSize = 0.0f; + + foreach (var index in Enumerable.Range(0, addon->UldManager.NodeListCount)) + { + var node = addon->UldManager.NodeList[index]; + + // Node 17 is the default CollisionNode that fits over the existing elements + if (node->NodeID is 17) totalSize += node->Width; + + // Node > 1000, are our custom nodes + if (node->NodeID is > 1000) totalSize += node->Width + this.configuration.DtrSpacing; + } + + return totalSize; + } + private bool AddNode(AtkTextNode* node) { var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; + this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); + this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); + this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); + var lastChild = dtr->RootNode->ChildNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; Log.Debug($"Found last sibling: {(ulong)lastChild:X}"); @@ -251,6 +364,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar Log.Debug("Set last sibling of DTR and updated child count"); dtr->UldManager.UpdateDrawNodeList(); + dtr->UpdateCollisionNodeList(false); Log.Debug("Updated node draw list"); return true; } @@ -260,6 +374,10 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; + this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)node, AddonEventType.MouseOver); + this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)node, AddonEventType.MouseOut); + this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)node, AddonEventType.MouseClick); + var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; @@ -272,25 +390,23 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1); Log.Debug("Set last sibling of DTR and updated child count"); dtr->UldManager.UpdateDrawNodeList(); + dtr->UpdateCollisionNodeList(false); Log.Debug("Updated node draw list"); return true; } private AtkTextNode* MakeNode(uint nodeId) { - var newTextNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8); + var newTextNode = IMemorySpace.GetUISpace()->Create(); if (newTextNode == null) { - Log.Debug("Failed to allocate memory for text node"); + Log.Debug("Failed to allocate memory for AtkTextNode"); return null; } - IMemorySpace.Memset(newTextNode, 0, (ulong)sizeof(AtkTextNode)); - newTextNode->Ctor(); - newTextNode->AtkResNode.NodeID = nodeId; newTextNode->AtkResNode.Type = NodeType.Text; - newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop; + newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents; newTextNode->AtkResNode.DrawFlags = 12; newTextNode->AtkResNode.SetWidth(22); newTextNode->AtkResNode.SetHeight(22); @@ -304,16 +420,49 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar newTextNode->SetText(" "); - newTextNode->TextColor.R = 255; - newTextNode->TextColor.G = 255; - newTextNode->TextColor.B = 255; - newTextNode->TextColor.A = 255; - - newTextNode->EdgeColor.R = 142; - newTextNode->EdgeColor.G = 106; - newTextNode->EdgeColor.B = 12; - newTextNode->EdgeColor.A = 255; + newTextNode->TextColor = new ByteColor { R = 255, G = 255, B = 255, A = 255 }; + newTextNode->EdgeColor = new ByteColor { R = 142, G = 106, B = 12, A = 255 }; return newTextNode; } + + private void DtrEventHandler(AddonEventType atkEventType, IntPtr atkUnitBase, IntPtr atkResNode) + { + var addon = (AtkUnitBase*)atkUnitBase; + var node = (AtkResNode*)atkResNode; + + if (this.entries.FirstOrDefault(entry => entry.TextNode == node) is not { } dtrBarEntry) return; + + if (dtrBarEntry is { Tooltip: not null }) + { + switch (atkEventType) + { + case AddonEventType.MouseOver: + AtkStage.GetSingleton()->TooltipManager.ShowTooltip(addon->ID, node, dtrBarEntry.Tooltip.Encode()); + break; + + case AddonEventType.MouseOut: + AtkStage.GetSingleton()->TooltipManager.HideTooltip(addon->ID); + break; + } + } + + if (dtrBarEntry is { OnClick: not null }) + { + switch (atkEventType) + { + case AddonEventType.MouseOver: + this.uiEventManager.SetCursor(AddonCursorType.Clickable); + break; + + case AddonEventType.MouseOut: + this.uiEventManager.ResetCursor(); + break; + + case AddonEventType.MouseClick: + dtrBarEntry.OnClick.Invoke(); + break; + } + } + } } diff --git a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs new file mode 100644 index 000000000..1e6fd09cd --- /dev/null +++ b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs @@ -0,0 +1,29 @@ +namespace Dalamud.Game.Gui.Dtr; + +/// +/// DtrBar memory address resolver. +/// +public class DtrBarAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the AtkUnitBaseDraw method. + /// This is the base handler for all addons. + /// We will use this here because _DTR does not have a overloaded handler, so we must use the base handler. + /// + public nint AtkUnitBaseDraw { get; private set; } + + /// + /// Gets the address of the DTRRequestUpdate method. + /// + public nint AddonRequestedUpdate { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(SigScanner scanner) + { + this.AtkUnitBaseDraw = scanner.ScanText("48 83 EC 28 F6 81 ?? ?? ?? ?? ?? 4C 8B C1"); + this.AddonRequestedUpdate = scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B BA ?? ?? ?? ?? 48 8B F1 49 8B 98 ?? ?? ?? ?? 33 D2"); + } +} diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index c5bdb7e85..f04e1427d 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -41,6 +41,16 @@ public sealed unsafe class DtrBarEntry : IDisposable this.Dirty = true; } } + + /// + /// Gets or sets a tooltip to be shown when the user mouses over the dtr entry. + /// + public SeString? Tooltip { get; set; } + + /// + /// Gets or sets a action to be invoked when the user clicks on the dtr entry. + /// + public Action? OnClick { get; set; } /// /// Gets or sets a value indicating whether this entry is visible. From 153f7c45bf1475ccc7dcf58e88a83eb1bfc0558e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 5 Sep 2023 00:46:45 -0700 Subject: [PATCH 050/122] Fix non-reversed resizing logic --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 4d2a005ae..b1679c296 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -249,6 +249,21 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (MemoryHelper.ReadString((nint)addon->Name, 0x20) is not "_DTR") return; this.UpdateNodePositions(addon); + + if (!this.configuration.DtrSwapDirection) + { + var targetSize = (ushort)this.CalculateTotalSize(); + var sizeDelta = targetSize - addon->RootNode->Width; + + if (addon->RootNode->Width != targetSize) + { + addon->RootNode->SetWidth(targetSize); + addon->SetX((short)(addon->GetX() - sizeDelta)); + + // force a RequestedUpdate immediately to force the game to right-justify it immediately. + this.onAddonRequestedUpdateHook.Original(addon, AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); + } + } } catch (Exception e) { @@ -258,13 +273,12 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private void UpdateNodePositions(AtkUnitBase* addon) { - var targetSize = (ushort)this.CalculateTotalSize(); - addon->RootNode->SetWidth(targetSize); - // If we grow to the right, we need to left-justify the original elements. // else if we grow to the left, the game right-justifies it for us. if (this.configuration.DtrSwapDirection) { + var targetSize = (ushort)this.CalculateTotalSize(); + addon->RootNode->SetWidth(targetSize); var sizeOffset = addon->GetNodeById(17)->GetX(); var node = addon->RootNode->ChildNode; @@ -338,7 +352,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (node->NodeID is 17) totalSize += node->Width; // Node > 1000, are our custom nodes - if (node->NodeID is > 1000) totalSize += node->Width + this.configuration.DtrSpacing; + if (node->NodeID is > 1000 && node->IsVisible) totalSize += node->Width + this.configuration.DtrSpacing; } return totalSize; From 166669597dea8e3d9735d457f06fe4056b257d9d Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:03:37 -0700 Subject: [PATCH 051/122] [DtrBar] Probably fix concurrency issues --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index b1679c296..8b021bc7a 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -50,6 +51,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private readonly DalamudAddonEventManager uiEventManager = Service.Get(); private readonly DtrBarAddressResolver address; + private readonly ConcurrentBag newEntries = new(); private readonly List entries = new(); private readonly Hook onAddonDrawHook; private readonly Hook onAddonRequestedUpdateHook; @@ -78,18 +80,17 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar /// public DtrBarEntry Get(string title, SeString? text = null) { - if (this.entries.Any(x => x.Title == title)) + if (this.entries.Any(x => x.Title == title) || this.newEntries.Any(x => x.Title == title)) throw new ArgumentException("An entry with the same title already exists."); - var node = this.MakeNode(++this.runningNodeIds); - var entry = new DtrBarEntry(title, node); + var entry = new DtrBarEntry(title, null); entry.Text = text; // Add the entry to the end of the order list, if it's not there already. if (!this.configuration.DtrOrder!.Contains(title)) this.configuration.DtrOrder!.Add(title); - this.entries.Add(entry); - this.ApplySort(); + + this.newEntries.Add(entry); return entry; } @@ -173,6 +174,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private void Update(Framework unused) { this.HandleRemovedNodes(); + this.HandleAddedNodes(); var dtr = this.GetDtr(); if (dtr == null) return; @@ -238,6 +240,21 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } + private void HandleAddedNodes() + { + if (this.newEntries.Any()) + { + foreach (var newEntry in this.newEntries) + { + newEntry.TextNode = this.MakeNode(++this.runningNodeIds); + this.entries.Add(newEntry); + } + + this.newEntries.Clear(); + this.ApplySort(); + } + } + // This hooks all AtkUnitBase.Draw calls, then checks for our specific addon name. // AddonDtr doesn't implement it's own Draw method, would need to replace vtable entry to be more efficient. private void OnAddonDrawDetour(AtkUnitBase* addon) From 692113958b3481aa75ce7d727d2ae158e143d619 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:35:08 -0700 Subject: [PATCH 052/122] Scope DTRBar --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 59 ++++++++++++++++++++++++++++-- Dalamud/Plugin/Services/IDtrBar.cs | 6 +++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 8b021bc7a..2ff99a450 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -26,9 +26,6 @@ namespace Dalamud.Game.Gui.Dtr; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; @@ -94,6 +91,15 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar return entry; } + + /// + public void Remove(string title) + { + if (this.entries.FirstOrDefault(entry => entry.Title == title) is { } dtrBarEntry) + { + dtrBarEntry.Remove(); + } + } /// void IDisposable.Dispose() @@ -497,3 +503,50 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } } + +/// +/// Plugin-scoped version of a AddonEventManager service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar +{ + [ServiceManager.ServiceDependency] + private readonly DtrBar dtrBarService = Service.Get(); + + private readonly Dictionary pluginEntries = new(); + + /// + public void Dispose() + { + foreach (var entry in this.pluginEntries) + { + entry.Value.Remove(); + } + + this.pluginEntries.Clear(); + } + + /// + public DtrBarEntry Get(string title, SeString? text = null) + { + // If we already have a known entry for this plugin, return it. + if (this.pluginEntries.TryGetValue(title, out var existingEntry)) return existingEntry; + + return this.pluginEntries[title] = this.dtrBarService.Get(title, text); + } + + /// + public void Remove(string title) + { + if (this.pluginEntries.TryGetValue(title, out var existingEntry)) + { + existingEntry.Remove(); + this.pluginEntries.Remove(title); + } + } +} diff --git a/Dalamud/Plugin/Services/IDtrBar.cs b/Dalamud/Plugin/Services/IDtrBar.cs index 6c2b8ad1e..a5a750cf6 100644 --- a/Dalamud/Plugin/Services/IDtrBar.cs +++ b/Dalamud/Plugin/Services/IDtrBar.cs @@ -19,4 +19,10 @@ public interface IDtrBar /// The entry object used to update, hide and remove the entry. /// Thrown when an entry with the specified title exists. public DtrBarEntry Get(string title, SeString? text = null); + + /// + /// Removes a DTR bar entry from the system. + /// + /// Title of the entry to remove. + public void Remove(string title); } From 4dabd0713171613d1fef4ece7015dd2879decc76 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:08:04 -0700 Subject: [PATCH 053/122] Prototype, untested --- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 63 ++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 185 ++++++++++++++---- .../AddonLifecycleEventListener.cs | 38 ++++ Dalamud/Hooking/Internal/CallHook.cs | 83 ++++++++ Dalamud/Plugin/Services/IAddonLifecycle.cs | 70 ++++++- 5 files changed, 393 insertions(+), 46 deletions(-) create mode 100644 Dalamud/Game/AddonLifecycle/AddonEvent.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs create mode 100644 Dalamud/Hooking/Internal/CallHook.cs diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs new file mode 100644 index 000000000..bf5ee75cf --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -0,0 +1,63 @@ +namespace Dalamud.Game.AddonLifecycle; + +/// +/// Enumeration for available AddonLifecycle events +/// +public enum AddonEvent +{ + /// + /// Event that is fired before an addon begins it's setup process. + /// + PreSetup, + + /// + /// Event that is fired after an addon has completed it's setup process. + /// + PostSetup, + + // // Events not implemented yet. + // /// + // /// Event that is fired right before an addon is set to shown. + // /// + // PreShow, + // + // /// + // /// Event that is fired after an addon has been shown. + // /// + // PostShow, + // + // /// + // /// Event that is fired right before an addon is set to hidden. + // /// + // PreHide, + // + // /// + // /// Event that is fired after an addon has been hidden. + // /// + // PostHide, + // + // /// + // /// Event that is fired before an addon begins update. + // /// + // PreUpdate, + // + // /// + // /// Event that is fired after an addon has completed update. + // /// + // PostUpdate, + // + // /// + // /// Event that is fired before an addon begins draw. + // /// + // PreDraw, + // + // /// + // /// Event that is fired after an addon has completed draw. + // /// + // PostDraw, + + /// + /// Event that is fired before an addon is finalized. + /// + PreFinalize, +} diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 72d1c25ff..d915bbd00 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using Dalamud.Hooking; using Dalamud.IoC; @@ -14,49 +17,99 @@ namespace Dalamud.Game.AddonLifecycle; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycle +internal unsafe class AddonLifecycle : IDisposable, IServiceType { private static readonly ModuleLog Log = new("AddonLifecycle"); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private readonly AddonLifecycleAddressResolver address; private readonly Hook onAddonSetupHook; private readonly Hook onAddonFinalizeHook; + + private readonly ConcurrentBag newEventListeners = new(); + private readonly ConcurrentBag removeEventListeners = new(); + private readonly List eventListeners = new(); [ServiceManager.ServiceConstructor] private AddonLifecycle(SigScanner sigScanner) { this.address = new AddonLifecycleAddressResolver(); this.address.Setup(sigScanner); + + this.framework.Update += this.OnFrameworkUpdate; this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); } - + private delegate nint AddonSetupDelegate(AtkUnitBase* addon); private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); - - /// - public event Action? AddonPreSetup; - - /// - public event Action? AddonPostSetup; - - /// - public event Action? AddonPreFinalize; - + /// public void Dispose() { + this.framework.Update -= this.OnFrameworkUpdate; + this.onAddonSetupHook.Dispose(); this.onAddonFinalizeHook.Dispose(); } + /// + /// Register a listener for the target event and addon. + /// + /// The listener to register. + internal void RegisterListener(AddonLifecycleEventListener listener) + { + this.newEventListeners.Add(listener); + } + + /// + /// Unregisters the listener from events. + /// + /// The listener to unregister. + internal void UnregisterListener(AddonLifecycleEventListener listener) + { + this.removeEventListeners.Add(listener); + } + + // Used to prevent concurrency issues if plugins try to register during iteration of listeners. + private void OnFrameworkUpdate(Framework unused) + { + if (this.newEventListeners.Any()) + { + this.eventListeners.AddRange(this.newEventListeners); + this.newEventListeners.Clear(); + } + + if (this.removeEventListeners.Any()) + { + foreach (var toRemoveListener in this.removeEventListeners) + { + this.eventListeners.Remove(toRemoveListener); + } + + this.removeEventListeners.Clear(); + } + } + [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { this.onAddonSetupHook.Enable(); this.onAddonFinalizeHook.Enable(); } + + private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) + { + // Match on string.empty for listeners that want events for all addons. + foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) + { + listener.FunctionDelegate.Invoke(eventType, args); + } + } private nint OnAddonSetup(AtkUnitBase* addon) { @@ -65,7 +118,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl try { - this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -76,7 +129,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl try { - this.AddonPostSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -96,7 +149,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl try { - this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + this.InvokeListeners(AddonEvent.PreFinalize, new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); } catch (Exception e) { @@ -118,39 +171,93 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl #pragma warning restore SA1015 internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle { + private static readonly ModuleLog Log = new("AddonLifecycle:PluginScoped"); + [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; - } - /// - public event Action? AddonPreSetup; - - /// - public event Action? AddonPostSetup; - - /// - public event Action? AddonPreFinalize; + private readonly List eventListeners = new(); /// public void Dispose() { - this.addonLifecycleService.AddonPreSetup -= this.AddonPreSetupForward; - this.addonLifecycleService.AddonPostSetup -= this.AddonPostSetupForward; - this.addonLifecycleService.AddonPreFinalize -= this.AddonPreFinalizeForward; + foreach (var listener in this.eventListeners) + { + this.addonLifecycleService.UnregisterListener(listener); + } } - private void AddonPreSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPreSetup?.Invoke(args); + /// + public void RegisterListener(AddonEvent eventType, IEnumerable addonNames, IAddonLifecycle.AddonEventDelegate handler) + { + foreach (var addonName in addonNames) + { + this.RegisterListener(eventType, addonName, handler); + } + } - private void AddonPostSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPostSetup?.Invoke(args); + /// + public void RegisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate handler) + { + var listener = new AddonLifecycleEventListener(eventType, addonName, handler); + this.eventListeners.Add(listener); + this.addonLifecycleService.RegisterListener(listener); + } + + /// + public void RegisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate handler) + { + this.RegisterListener(eventType, string.Empty, handler); + } + + /// + public void UnregisterListener(AddonEvent eventType, IEnumerable addonNames, IAddonLifecycle.AddonEventDelegate? handler = null) + { + foreach (var addonName in addonNames) + { + this.UnregisterListener(eventType, addonName, handler); + } + } - private void AddonPreFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPreFinalize?.Invoke(args); + /// + public void UnregisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate? handler = null) + { + // This style is simpler to read imo. If the handler is null we want all entries, + // if they specified a handler then only the specific entries with that handler. + var targetListeners = this.eventListeners + .Where(entry => entry.EventType == eventType) + .Where(entry => entry.AddonName == addonName) + .Where(entry => handler is null || entry.FunctionDelegate == handler); + + foreach (var listener in targetListeners) + { + this.addonLifecycleService.UnregisterListener(listener); + this.eventListeners.Remove(listener); + } + } + + /// + public void UnregisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate? handler = null) + { + this.UnregisterListener(eventType, string.Empty, handler); + } + + /// + public void UnregisterListener(IAddonLifecycle.AddonEventDelegate handler, params IAddonLifecycle.AddonEventDelegate[] handlers) + { + foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handler)) + { + this.addonLifecycleService.UnregisterListener(listener); + this.eventListeners.Remove(listener); + } + + foreach (var handlerParma in handlers) + { + foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handlerParma)) + { + this.addonLifecycleService.UnregisterListener(listener); + this.eventListeners.Remove(listener); + } + } + } } diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs new file mode 100644 index 000000000..0f088362d --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs @@ -0,0 +1,38 @@ +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.AddonLifecycle; + +/// +/// This class is a helper for tracking and invoking listener delegates. +/// +internal class AddonLifecycleEventListener +{ + /// + /// Initializes a new instance of the class. + /// + /// Event type to listen for. + /// Addon name to listen for. + /// Delegate to invoke. + internal AddonLifecycleEventListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate functionDelegate) + { + this.EventType = eventType; + this.AddonName = addonName; + this.FunctionDelegate = functionDelegate; + } + + /// + /// Gets the name of the addon this listener is looking for. + /// string.Empty if it wants to be called for any addon. + /// + public string AddonName { get; init; } + + /// + /// Gets the event type this listener is looking for. + /// + public AddonEvent EventType { get; init; } + + /// + /// Gets the delegate this listener invokes. + /// + public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; } +} diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs new file mode 100644 index 000000000..0f8c681c2 --- /dev/null +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -0,0 +1,83 @@ +using System; +using System.Runtime.InteropServices; + +using Reloaded.Hooks.Definitions; + +namespace Dalamud.Hooking.Internal; + +/// +/// Hooking class for callsite hooking. This hook does not have capabilities of calling the original function. +/// The intended use is replacing virtual function calls where you are able to manually invoke the original call using the delegate arguments. +/// +/// Delegate signature for this hook. +internal class CallHook : IDisposable where T : Delegate +{ + private readonly Reloaded.Hooks.AsmHook asmHook; + + private T? detour; + private bool activated; + + /// + /// Initializes a new instance of the class. + /// + /// Address of the instruction to replace. + /// Delegate to invoke. + internal CallHook(nint address, T detour) + { + this.detour = detour; + + var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); + var code = new[] + { + "use64", + $"mov rax, 0x{detourPtr:X8}", + "call rax", + }; + + var opt = new AsmHookOptions + { + PreferRelativeJump = true, + Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, + MaxOpcodeSize = 5, + }; + + this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); + } + + /// + /// Gets a value indicating whether or not the hook is enabled. + /// + public bool IsEnabled => this.asmHook.IsEnabled; + + /// + /// Starts intercepting a call to the function. + /// + public void Enable() + { + if (!this.activated) + { + this.activated = true; + this.asmHook.Activate(); + return; + } + + this.asmHook.Enable(); + } + + /// + /// Stops intercepting a call to the function. + /// + public void Disable() + { + this.asmHook.Disable(); + } + + /// + /// Remove a hook from the current process. + /// + public void Dispose() + { + this.asmHook.Disable(); + this.detour = null; + } +} diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 43b9fef0a..1e318ae79 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -1,5 +1,7 @@ -using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Dalamud.Game.AddonLifecycle; using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -11,19 +13,73 @@ namespace Dalamud.Plugin.Services; public interface IAddonLifecycle { /// - /// Event that fires before an addon is being setup. + /// Delegate for receiving addon lifecycle event messages. /// - public event Action AddonPreSetup; + /// The event type that triggered the message. + /// Information about what addon triggered the message. + public delegate void AddonEventDelegate(AddonEvent eventType, AddonArgs addonInfo); /// - /// Event that fires after an addon is done being setup. + /// Register a listener that will trigger on the specified event and any of the specified addons. /// - public event Action AddonPostSetup; + /// Event type to trigger on. + /// Addon names that will trigger the handler to be invoked. + /// The handler to invoke. + void RegisterListener(AddonEvent eventType, IEnumerable addonNames, AddonEventDelegate handler); /// - /// Event that fires before an addon is being finalized. + /// Register a listener that will trigger on the specified event only for the specified addon. /// - public event Action AddonPreFinalize; + /// Event type to trigger on. + /// The addon name that will trigger the handler to be invoked. + /// The handler to invoke. + void RegisterListener(AddonEvent eventType, string addonName, AddonEventDelegate handler); + + /// + /// Register a listener that will trigger on the specified event for any addon. + /// + /// Event type to trigger on. + /// The handler to invoke. + void RegisterListener(AddonEvent eventType, AddonEventDelegate handler); + + /// + /// Unregister listener from specified event type and specified addon names. + /// + /// + /// If a specific handler is not provided, all handlers for the event type and addon names will be unregistered. + /// + /// Event type to deregister. + /// Addon names to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AddonEvent eventType, IEnumerable addonNames, [Optional] AddonEventDelegate handler); + + /// + /// Unregister all listeners for the specified event type and addon name. + /// + /// + /// If a specific handler is not provided, all handlers for the event type and addons will be unregistered. + /// + /// Event type to deregister. + /// Addon name to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AddonEvent eventType, string addonName, [Optional] AddonEventDelegate handler); + + /// + /// Unregister an event type handler.
This will only remove a handler that is added via . + ///
+ /// + /// If a specific handler is not provided, all handlers for the event type and addons will be unregistered. + /// + /// Event type to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AddonEvent eventType, [Optional] AddonEventDelegate handler); + + /// + /// Unregister all events that use the specified handlers. + /// + /// Event handler to remove. + /// Additional handlers to remove. + void UnregisterListener(AddonEventDelegate handler, params AddonEventDelegate[] handlers); /// /// Addon argument data for use in event subscribers. From 9176342ad5b4978b1f3849758d8093ee3768cbd9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 6 Sep 2023 16:00:37 -0700 Subject: [PATCH 054/122] Add AddonDraw, AddonUpdate --- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 67 ++++++----------- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 74 +++++++++++++++++-- .../AddonLifecycleAddressResolver.cs | 16 +++- 3 files changed, 106 insertions(+), 51 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs index bf5ee75cf..0125d1337 100644 --- a/Dalamud/Game/AddonLifecycle/AddonEvent.cs +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -1,7 +1,7 @@ namespace Dalamud.Game.AddonLifecycle; /// -/// Enumeration for available AddonLifecycle events +/// Enumeration for available AddonLifecycle events. /// public enum AddonEvent { @@ -9,53 +9,32 @@ public enum AddonEvent /// Event that is fired before an addon begins it's setup process. /// PreSetup, - + /// /// Event that is fired after an addon has completed it's setup process. /// PostSetup, - - // // Events not implemented yet. - // /// - // /// Event that is fired right before an addon is set to shown. - // /// - // PreShow, - // - // /// - // /// Event that is fired after an addon has been shown. - // /// - // PostShow, - // - // /// - // /// Event that is fired right before an addon is set to hidden. - // /// - // PreHide, - // - // /// - // /// Event that is fired after an addon has been hidden. - // /// - // PostHide, - // - // /// - // /// Event that is fired before an addon begins update. - // /// - // PreUpdate, - // - // /// - // /// Event that is fired after an addon has completed update. - // /// - // PostUpdate, - // - // /// - // /// Event that is fired before an addon begins draw. - // /// - // PreDraw, - // - // /// - // /// Event that is fired after an addon has completed draw. - // /// - // PostDraw, - + + /// + /// Event that is fired before an addon begins update. + /// + PreUpdate, + + /// + /// Event that is fired after an addon has completed update. + /// + PostUpdate, + + /// + /// Event that is fired before an addon begins draw. + /// + PreDraw, + + /// + /// Event that is fired after an addon has completed draw. + /// + PostDraw, + /// /// Event that is fired before an addon is finalized. /// diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index d915bbd00..3a8644c4c 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Hooking; +using Dalamud.Hooking.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -27,6 +28,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly AddonLifecycleAddressResolver address; private readonly Hook onAddonSetupHook; private readonly Hook onAddonFinalizeHook; + private readonly CallHook onAddonDrawHook; + private readonly CallHook onAddonUpdateHook; private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); @@ -42,12 +45,18 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); + this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); + this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); } - + private delegate nint AddonSetupDelegate(AtkUnitBase* addon); private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + private delegate void AddonDrawDelegate(AtkUnitBase* addon); + + private delegate void AddonUpdateDelegate(AtkUnitBase* addon); + /// public void Dispose() { @@ -55,6 +64,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonSetupHook.Dispose(); this.onAddonFinalizeHook.Dispose(); + this.onAddonDrawHook.Dispose(); + this.onAddonUpdateHook.Dispose(); } /// @@ -100,6 +111,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { this.onAddonSetupHook.Enable(); this.onAddonFinalizeHook.Enable(); + this.onAddonDrawHook.Enable(); + this.onAddonUpdateHook.Enable(); } private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) @@ -158,6 +171,56 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); } + + private void OnAddonDraw(AtkUnitBase* addon) + { + if (addon is null) return; + + try + { + this.InvokeListeners(AddonEvent.PreDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); + } + + ((delegate* unmanaged)addon->AtkEventListener.vfunc[42])(addon); + + try + { + this.InvokeListeners(AddonEvent.PostDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonDraw post-draw invoke."); + } + } + + private void OnAddonUpdate(AtkUnitBase* addon) + { + if (addon is null) return; + + try + { + this.InvokeListeners(AddonEvent.PreUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonUpdate pre-update invoke."); + } + + ((delegate* unmanaged)addon->AtkEventListener.vfunc[41])(addon); + + try + { + this.InvokeListeners(AddonEvent.PostUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); + } + } } /// @@ -175,7 +238,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); - + private readonly List eventListeners = new(); /// @@ -227,7 +290,8 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif var targetListeners = this.eventListeners .Where(entry => entry.EventType == eventType) .Where(entry => entry.AddonName == addonName) - .Where(entry => handler is null || entry.FunctionDelegate == handler); + .Where(entry => handler is null || entry.FunctionDelegate == handler) + .ToArray(); // Make a copy so we don't mutate this list while removing entries. foreach (var listener in targetListeners) { @@ -245,7 +309,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif /// public void UnregisterListener(IAddonLifecycle.AddonEventDelegate handler, params IAddonLifecycle.AddonEventDelegate[] handlers) { - foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handler)) + foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handler).ToArray()) { this.addonLifecycleService.UnregisterListener(listener); this.eventListeners.Remove(listener); @@ -253,7 +317,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif foreach (var handlerParma in handlers) { - foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handlerParma)) + foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handlerParma).ToArray()) { this.addonLifecycleService.UnregisterListener(listener); this.eventListeners.Remove(listener); diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index ba7b723ec..0300667a9 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -6,14 +6,24 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// - /// Gets the address of the addon setup hook invoked by the atkunitmanager. + /// 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. + /// Gets the address of the addon finalize hook invoked by the AtkUnitManager. /// public nint AddonFinalize { get; private set; } + + /// + /// Gets the address of the addon draw hook invoked by virtual function call. + /// + public nint AddonDraw { get; private set; } + + /// + /// Gets the address of the addon update hook invoked by virtual function call. + /// + public nint AddonUpdate { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -23,5 +33,7 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver { this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); + this.AddonDraw = sig.ScanText("48 8B 01 FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); + this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); } } From 8cb76a7438ee668a413f1cbb604f0c2c045379c0 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 6 Sep 2023 20:40:46 -0700 Subject: [PATCH 055/122] Fix incorrect AtkUnitBase.Update function delegate definition --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 14 +++++--------- .../AddonLifecycleAddressResolver.cs | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 3a8644c4c..a3e9f4be8 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -55,7 +55,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate void AddonDrawDelegate(AtkUnitBase* addon); - private delegate void AddonUpdateDelegate(AtkUnitBase* addon); + private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta); /// public void Dispose() @@ -174,8 +174,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonDraw(AtkUnitBase* addon) { - if (addon is null) return; - try { this.InvokeListeners(AddonEvent.PreDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); @@ -185,7 +183,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); } - ((delegate* unmanaged)addon->AtkEventListener.vfunc[42])(addon); + addon->Draw(); try { @@ -197,10 +195,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - private void OnAddonUpdate(AtkUnitBase* addon) + private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - if (addon is null) return; - try { this.InvokeListeners(AddonEvent.PreUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); @@ -209,8 +205,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonUpdate pre-update invoke."); } - - ((delegate* unmanaged)addon->AtkEventListener.vfunc[41])(addon); + + addon->Update(delta); try { diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index 0300667a9..688476d82 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -33,7 +33,7 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver { this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); - this.AddonDraw = sig.ScanText("48 8B 01 FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); + this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); } } 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 056/122] 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 057/122] 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 058/122] 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 059/122] 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 060/122] 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 061/122] 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 6a0401646ff99ea845fb16e7f48a7e3e1c7314dd Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 7 Sep 2023 11:47:35 -0700 Subject: [PATCH 062/122] Add AddonOnRequestedUpdate --- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 10 ++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 31 +++++++++++++++++++ .../AddonLifecycleAddressResolver.cs | 6 ++++ 3 files changed, 47 insertions(+) diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs index 0125d1337..30121d162 100644 --- a/Dalamud/Game/AddonLifecycle/AddonEvent.cs +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -39,4 +39,14 @@ public enum AddonEvent /// Event that is fired before an addon is finalized. /// PreFinalize, + + /// + /// Event that is fired before an addon begins a requested update. + /// + PreRequestedUpdate, + + /// + /// Event that is fired after an addon finishes a requested update. + /// + PostRequestedUpdate, } diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index a3e9f4be8..9bc792f46 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -30,6 +30,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly Hook onAddonFinalizeHook; private readonly CallHook onAddonDrawHook; private readonly CallHook onAddonUpdateHook; + // private readonly CallHook onAddonRequestedUpdateHook; // See Note in Ctor private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); @@ -47,6 +48,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); + + // todo: reenable this. WARNING: This hook overwrites a system that SimpleTweaks uses, causing SimpleTweaks to report exceptions. + // this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); } private delegate nint AddonSetupDelegate(AtkUnitBase* addon); @@ -57,6 +61,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta); + private delegate void AddonOnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + /// public void Dispose() { @@ -66,6 +72,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Dispose(); this.onAddonDrawHook.Dispose(); this.onAddonUpdateHook.Dispose(); + // this.onAddonRequestedUpdateHook.Dispose(); // See Note in Ctor } /// @@ -113,6 +120,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Enable(); this.onAddonDrawHook.Enable(); this.onAddonUpdateHook.Enable(); + // this.onAddonRequestedUpdateHook.Enable(); // See Note in Ctor } private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) @@ -217,6 +225,29 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); } } + + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + try + { + this.InvokeListeners(AddonEvent.PreRequestedUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke."); + } + + addon->OnUpdate(numberArrayData, stringArrayData); + + try + { + this.InvokeListeners(AddonEvent.PostRequestedUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke."); + } + } } /// diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index 688476d82..557cedc34 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -24,6 +24,11 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver /// Gets the address of the addon update hook invoked by virtual function call. /// public nint AddonUpdate { get; private set; } + + /// + /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. + /// + public nint AddonOnRequestedUpdate { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -35,5 +40,6 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); + this.AddonOnRequestedUpdate = sig.ScanText("FF 90 90 01 00 00 48 8B 5C 24 30 48 83 C4 20"); } } From e54b09a3c5c132ac2989cbf682bf4a35b82d0064 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 7 Sep 2023 12:24:45 -0700 Subject: [PATCH 063/122] Add Addon.OnRefresh --- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 10 ++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 31 ++++++++++++++++++- .../AddonLifecycleAddressResolver.cs | 6 ++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs index 30121d162..faef30c88 100644 --- a/Dalamud/Game/AddonLifecycle/AddonEvent.cs +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -49,4 +49,14 @@ public enum AddonEvent /// Event that is fired after an addon finishes a requested update. /// PostRequestedUpdate, + + /// + /// Event that is fired before an addon begins a refresh. + /// + PreRefresh, + + /// + /// Event that is fired after an addon has finished a refresh. + /// + PostRefresh, } diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 9bc792f46..d6da18dd5 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -30,6 +30,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly Hook onAddonFinalizeHook; private readonly CallHook onAddonDrawHook; private readonly CallHook onAddonUpdateHook; + private readonly Hook onAddonRefreshHook; // private readonly CallHook onAddonRequestedUpdateHook; // See Note in Ctor private readonly ConcurrentBag newEventListeners = new(); @@ -48,6 +49,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); + this.onAddonRefreshHook = Hook.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh); // todo: reenable this. WARNING: This hook overwrites a system that SimpleTweaks uses, causing SimpleTweaks to report exceptions. // this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); @@ -61,7 +63,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta); - private delegate void AddonOnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + private delegate void AddonOnRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + + private delegate void AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); /// public void Dispose() @@ -72,6 +76,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Dispose(); this.onAddonDrawHook.Dispose(); this.onAddonUpdateHook.Dispose(); + this.onAddonRefreshHook.Dispose(); // this.onAddonRequestedUpdateHook.Dispose(); // See Note in Ctor } @@ -120,6 +125,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Enable(); this.onAddonDrawHook.Enable(); this.onAddonUpdateHook.Enable(); + this.onAddonRefreshHook.Enable(); // this.onAddonRequestedUpdateHook.Enable(); // See Note in Ctor } @@ -226,6 +232,29 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } + private void OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + try + { + this.InvokeListeners(AddonEvent.PreRefresh, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke."); + } + + this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values); + + try + { + this.InvokeListeners(AddonEvent.PostRefresh, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); + } + } + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { try diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index 557cedc34..079e09c80 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -29,6 +29,11 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. /// public nint AddonOnRequestedUpdate { get; private set; } + + /// + /// Gets the address of AtkUnitManager_vf10 which triggers addon onRefresh. + /// + public nint AddonOnRefresh { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -41,5 +46,6 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); this.AddonOnRequestedUpdate = sig.ScanText("FF 90 90 01 00 00 48 8B 5C 24 30 48 83 C4 20"); + this.AddonOnRefresh = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 41 8B F8 48 8B DA"); } } From 371b8a9dccb6324114c763d83f11e70ea60fb1df Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 00:16:25 -0700 Subject: [PATCH 064/122] Restore AddonOnRequestedUpdate --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index d6da18dd5..321c60a15 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -31,7 +31,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly CallHook onAddonDrawHook; private readonly CallHook onAddonUpdateHook; private readonly Hook onAddonRefreshHook; - // private readonly CallHook onAddonRequestedUpdateHook; // See Note in Ctor + private readonly CallHook onAddonRequestedUpdateHook; private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); @@ -50,9 +50,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); this.onAddonRefreshHook = Hook.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh); - - // todo: reenable this. WARNING: This hook overwrites a system that SimpleTweaks uses, causing SimpleTweaks to report exceptions. - // this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); + this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); } private delegate nint AddonSetupDelegate(AtkUnitBase* addon); @@ -77,7 +75,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonDrawHook.Dispose(); this.onAddonUpdateHook.Dispose(); this.onAddonRefreshHook.Dispose(); - // this.onAddonRequestedUpdateHook.Dispose(); // See Note in Ctor + this.onAddonRequestedUpdateHook.Dispose(); } /// @@ -126,7 +124,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonDrawHook.Enable(); this.onAddonUpdateHook.Enable(); this.onAddonRefreshHook.Enable(); - // this.onAddonRequestedUpdateHook.Enable(); // See Note in Ctor + this.onAddonRequestedUpdateHook.Enable(); } private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) From e914f339906b8656211d7efd4da678e416e93d35 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 01:20:29 -0700 Subject: [PATCH 065/122] Remove redundant null checking, and unused module log. There's no reasonable way the args passed in will be null. There's tons of null checking that square does before passing the pointers to these functions. If they are null, then the game has likely already crashed. --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 321c60a15..014033312 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -138,9 +138,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private nint OnAddonSetup(AtkUnitBase* addon) { - if (addon is null) - return this.onAddonSetupHook.Original(addon); - try { this.InvokeListeners(AddonEvent.PreSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); @@ -166,12 +163,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { - if (atkUnitBase is null || atkUnitBase[0] is null) - { - this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); - return; - } - try { this.InvokeListeners(AddonEvent.PreFinalize, new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); @@ -288,8 +279,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType #pragma warning restore SA1015 internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle { - private static readonly ModuleLog Log = new("AddonLifecycle:PluginScoped"); - [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); From 967c79fdd318acba110772fea546e820b9c0fe2c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 02:33:27 -0700 Subject: [PATCH 066/122] Improve logic for Unregistering Listeners --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 014033312..c0094d11a 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -21,10 +21,10 @@ namespace Dalamud.Game.AddonLifecycle; internal unsafe class AddonLifecycle : IDisposable, IServiceType { private static readonly ModuleLog Log = new("AddonLifecycle"); - + [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - + private readonly AddonLifecycleAddressResolver address; private readonly Hook onAddonSetupHook; private readonly Hook onAddonFinalizeHook; @@ -36,13 +36,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); private readonly List eventListeners = new(); - + [ServiceManager.ServiceConstructor] private AddonLifecycle(SigScanner sigScanner) { this.address = new AddonLifecycleAddressResolver(); this.address.Setup(sigScanner); - + this.framework.Update += this.OnFrameworkUpdate; this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); @@ -69,7 +69,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType public void Dispose() { this.framework.Update -= this.OnFrameworkUpdate; - + this.onAddonSetupHook.Dispose(); this.onAddonFinalizeHook.Dispose(); this.onAddonDrawHook.Dispose(); @@ -77,7 +77,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRefreshHook.Dispose(); this.onAddonRequestedUpdateHook.Dispose(); } - + /// /// Register a listener for the target event and addon. /// @@ -95,7 +95,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { this.removeEventListeners.Add(listener); } - + // Used to prevent concurrency issues if plugins try to register during iteration of listeners. private void OnFrameworkUpdate(Framework unused) { @@ -111,11 +111,11 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { this.eventListeners.Remove(toRemoveListener); } - + this.removeEventListeners.Clear(); } } - + [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { @@ -126,7 +126,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRefreshHook.Enable(); this.onAddonRequestedUpdateHook.Enable(); } - + private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) { // Match on string.empty for listeners that want events for all addons. @@ -174,7 +174,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); } - + private void OnAddonDraw(AtkUnitBase* addon) { try @@ -185,7 +185,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); } - + addon->Draw(); try @@ -197,7 +197,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonDraw post-draw invoke."); } } - + private void OnAddonUpdate(AtkUnitBase* addon, float delta) { try @@ -220,7 +220,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); } } - + private void OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) { try @@ -243,7 +243,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); } } - + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { try @@ -283,7 +283,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif private readonly AddonLifecycle addonLifecycleService = Service.Get(); private readonly List eventListeners = new(); - + /// public void Dispose() { @@ -301,7 +301,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif this.RegisterListener(eventType, addonName, handler); } } - + /// public void RegisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate handler) { @@ -324,31 +324,27 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif this.UnregisterListener(eventType, addonName, handler); } } - + /// public void UnregisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate? handler = null) { - // This style is simpler to read imo. If the handler is null we want all entries, - // if they specified a handler then only the specific entries with that handler. - var targetListeners = this.eventListeners - .Where(entry => entry.EventType == eventType) - .Where(entry => entry.AddonName == addonName) - .Where(entry => handler is null || entry.FunctionDelegate == handler) - .ToArray(); // Make a copy so we don't mutate this list while removing entries. - - foreach (var listener in targetListeners) + this.eventListeners.RemoveAll(entry => { - this.addonLifecycleService.UnregisterListener(listener); - this.eventListeners.Remove(listener); - } + if (entry.EventType != eventType) return false; + if (entry.AddonName != addonName) return false; + if (handler is not null && entry.FunctionDelegate != handler) return false; + + this.addonLifecycleService.UnregisterListener(entry); + return true; + }); } - + /// public void UnregisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate? handler = null) { this.UnregisterListener(eventType, string.Empty, handler); } - + /// public void UnregisterListener(IAddonLifecycle.AddonEventDelegate handler, params IAddonLifecycle.AddonEventDelegate[] handlers) { 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 067/122] 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) { From 1b4bee3d1302a4a779dd6efa396f0ad1cf8008ae Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 21:07:03 -0700 Subject: [PATCH 068/122] Remove array copy of handlers. --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 20 ++++++++----------- Dalamud/Plugin/Services/IAddonLifecycle.cs | 5 ++--- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index c0094d11a..c0e817f0d 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -346,21 +346,17 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif } /// - public void UnregisterListener(IAddonLifecycle.AddonEventDelegate handler, params IAddonLifecycle.AddonEventDelegate[] handlers) + public void UnregisterListener(params IAddonLifecycle.AddonEventDelegate[] handlers) { - foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handler).ToArray()) + foreach (var handler in handlers) { - this.addonLifecycleService.UnregisterListener(listener); - this.eventListeners.Remove(listener); - } - - foreach (var handlerParma in handlers) - { - foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handlerParma).ToArray()) + this.eventListeners.RemoveAll(entry => { - this.addonLifecycleService.UnregisterListener(listener); - this.eventListeners.Remove(listener); - } + if (entry.FunctionDelegate != handler) return false; + + this.addonLifecycleService.UnregisterListener(entry); + return true; + }); } } } diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 1e318ae79..cbb3d7c24 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -77,9 +77,8 @@ public interface IAddonLifecycle /// /// Unregister all events that use the specified handlers. /// - /// Event handler to remove. - /// Additional handlers to remove. - void UnregisterListener(AddonEventDelegate handler, params AddonEventDelegate[] handlers); + /// Handlers to remove. + void UnregisterListener(params AddonEventDelegate[] handlers); /// /// Addon argument data for use in event subscribers. From 764e0a81b7caf732846c2363b48fa68e642fe3d3 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 9 Sep 2023 15:10:52 -0700 Subject: [PATCH 069/122] Fix log filtering with IPluginLog - Rename `PluginLog`'s property to `Dalamud.PluginName` to match what `IPluginLog` is doing. - Change `ModuleLog` to use `Dalamud.ModuleName` as its context property. - Update the Console window to handle both changes. - Add the ability to filter to only Dalamud module log messages. --- .../Internal/Windows/ConsoleWindow.cs | 34 +++++++++++++------ Dalamud/Logging/Internal/ModuleLog.cs | 11 +++--- Dalamud/Logging/PluginLog.cs | 2 +- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 872fdcd37..3303a2280 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -180,17 +180,20 @@ internal class ConsoleWindow : Window, IDisposable } // Filter by specific plugin(s) - var pluginInternalNames = Service.Get().InstalledPlugins + var sourceNames = Service.Get().InstalledPlugins .Select(p => p.Manifest.InternalName) - .OrderBy(s => s).ToList(); + .OrderBy(s => s) + .Prepend("DalamudInternal") + .ToList(); + var sourcePreviewVal = this.sourceFilters.Count switch { - 0 => "All plugins...", - 1 => "1 plugin...", - _ => $"{this.sourceFilters.Count} plugins...", + 0 => "All sources...", + 1 => "1 source...", + _ => $"{this.sourceFilters.Count} sources...", }; - var sourceSelectables = pluginInternalNames.Union(this.sourceFilters).ToList(); - if (ImGui.BeginCombo("Plugins", sourcePreviewVal)) + var sourceSelectables = sourceNames.Union(this.sourceFilters).ToList(); + if (ImGui.BeginCombo("Sources", sourcePreviewVal)) { foreach (var selectable in sourceSelectables) { @@ -443,7 +446,8 @@ internal class ConsoleWindow : Window, IDisposable // TODO: Improve this, add partial completion // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484 - var candidates = Service.Get().Commands.Where(x => x.Key.Contains("/" + words[0])).ToList(); + var candidates = Service.Get().Commands.Where(x => x.Key.Contains("/" + words[0])) + .ToList(); if (candidates.Count > 0) { ptr.DeleteChars(0, ptr.BufTextLen); @@ -499,9 +503,13 @@ internal class ConsoleWindow : Window, IDisposable TimeStamp = logEvent.Timestamp, HasException = logEvent.Exception != null, }; - - if (logEvent.Properties.TryGetValue("SourceContext", out var sourceProp) && - sourceProp is ScalarValue { Value: string value }) + + if (logEvent.Properties.ContainsKey("Dalamud.ModuleName")) + { + entry.Source = "DalamudInternal"; + } + else if (logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) && + sourceProp is ScalarValue { Value: string value }) { entry.Source = value; } @@ -579,6 +587,10 @@ internal class ConsoleWindow : Window, IDisposable public bool IsMultiline { get; set; } + /// + /// Gets or sets the system responsible for generating this log entry. Generally will be a plugin's + /// InternalName. + /// public string? Source { get; set; } public bool HasException { get; set; } diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index c6c66e81a..2fb735640 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -12,6 +12,10 @@ public class ModuleLog { private readonly string moduleName; private readonly ILogger moduleLogger; + + // FIXME (v9): Deprecate this class in favor of using contextualized ILoggers with proper formatting. + // We can keep this class around as a Serilog helper, but ModuleLog should no longer be a returned + // type, instead returning a (prepared) ILogger appropriately. /// /// Initializes a new instance of the class. @@ -20,10 +24,8 @@ public class ModuleLog /// The module name. public ModuleLog(string? moduleName) { - // FIXME: Should be namespaced better, e.g. `Dalamud.PluginLoader`, but that becomes a relatively large - // change. this.moduleName = moduleName ?? "DalamudInternal"; - this.moduleLogger = Log.ForContext("SourceContext", this.moduleName); + this.moduleLogger = Log.ForContext("Dalamud.ModuleName", this.moduleName); } /// @@ -128,7 +130,8 @@ public class ModuleLog public void Fatal(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); - private void WriteLog(LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) + private void WriteLog( + LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) { // FIXME: Eventually, the `pluginName` tag should be removed from here and moved over to the actual log // formatter. diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index b2f2a5065..3ac98f15a 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -256,7 +256,7 @@ public static class PluginLog private static ILogger GetPluginLogger(string? pluginName) { - return Serilog.Log.ForContext("SourceContext", pluginName ?? string.Empty); + return Serilog.Log.ForContext("Dalamud.PluginName", pluginName ?? string.Empty); } private static void WriteLog(string? pluginName, LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) From f1c8201f1b027f0e35400f1899e53bd0dd8ffe07 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:49:04 -0700 Subject: [PATCH 070/122] Use vfunc call instead of hook --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 2ff99a450..ae01d4886 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -284,7 +284,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar addon->SetX((short)(addon->GetX() - sizeDelta)); // force a RequestedUpdate immediately to force the game to right-justify it immediately. - this.onAddonRequestedUpdateHook.Original(addon, AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); + addon->OnUpdate(AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); } } } From c9a5c7c4c53d3af8280b5ff809f18c1ba3f3e82c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 9 Sep 2023 20:21:26 -0700 Subject: [PATCH 071/122] Add AddonLifecycle to Self-Test --- Dalamud/Hooking/Internal/CallHook.cs | 10 +- .../AgingSteps/AddonLifecycleAgingStep.cs | 133 ++++++++++++++++++ .../Windows/SelfTest/SelfTestWindow.cs | 1 + 3 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs index 0f8c681c2..2bef59c86 100644 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -6,8 +6,14 @@ using Reloaded.Hooks.Definitions; namespace Dalamud.Hooking.Internal; /// -/// Hooking class for callsite hooking. This hook does not have capabilities of calling the original function. -/// The intended use is replacing virtual function calls where you are able to manually invoke the original call using the delegate arguments. +/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook. +/// This is a destructive operation, no other callsite hooks can coexist at the same address. +/// +/// There's no .Original for this hook type. +/// This is only intended for be for functions where the parameters provided allow you to invoke the original call. +/// +/// This class was specifically added for hooking virtual function callsites. +/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered. /// /// Delegate signature for this hook. internal class CallHook : IDisposable where T : Delegate diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs new file mode 100644 index 000000000..9dcaec558 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; + +using Dalamud.Game.AddonLifecycle; +using Dalamud.Plugin.Services; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; + +/// +/// Test setup AddonLifecycle Service. +/// +internal class AddonLifecycleAgingStep : IAgingStep +{ + private readonly List listeners; + + private AddonLifecycle? service; + private TestStep currentStep = TestStep.CharacterRefresh; + private bool listenersRegistered; + + /// + /// Initializes a new instance of the class. + /// + public AddonLifecycleAgingStep() + { + this.listeners = new List + { + new(AddonEvent.PostSetup, "Character", this.PostSetup), + new(AddonEvent.PostUpdate, "Character", this.PostUpdate), + new(AddonEvent.PostDraw, "Character", this.PostDraw), + new(AddonEvent.PostRefresh, "Character", this.PostRefresh), + new(AddonEvent.PostRequestedUpdate, "Character", this.PostRequestedUpdate), + new(AddonEvent.PreFinalize, "Character", this.PreFinalize), + }; + } + + private enum TestStep + { + CharacterRefresh, + CharacterSetup, + CharacterRequestedUpdate, + CharacterUpdate, + CharacterDraw, + CharacterFinalize, + Complete, + } + + /// + public string Name => "Test AddonLifecycle"; + + /// + public SelfTestStepResult RunStep() + { + this.service ??= Service.Get(); + if (this.service is null) return SelfTestStepResult.Fail; + + if (!this.listenersRegistered) + { + foreach (var listener in this.listeners) + { + this.service.RegisterListener(listener); + } + + this.listenersRegistered = true; + } + + switch (this.currentStep) + { + case TestStep.CharacterRefresh: + ImGui.Text("Open Character Window."); + break; + + case TestStep.CharacterSetup: + ImGui.Text("Open Character Window."); + break; + + case TestStep.CharacterRequestedUpdate: + ImGui.Text("Change tabs, or un-equip/equip gear."); + break; + + case TestStep.CharacterFinalize: + ImGui.Text("Close Character Window."); + break; + + case TestStep.CharacterUpdate: + case TestStep.CharacterDraw: + case TestStep.Complete: + default: + // Nothing to report to tester. + break; + } + + return this.currentStep is TestStep.Complete ? SelfTestStepResult.Pass : SelfTestStepResult.Waiting; + } + + /// + public void CleanUp() + { + foreach (var listener in this.listeners) + { + this.service?.UnregisterListener(listener); + } + } + + private void PostSetup(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterSetup) this.currentStep++; + } + + private void PostUpdate(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterUpdate) this.currentStep++; + } + + private void PostDraw(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterDraw) this.currentStep++; + } + + private void PostRefresh(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterRefresh) this.currentStep++; + } + + private void PostRequestedUpdate(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterRequestedUpdate) this.currentStep++; + } + + private void PreFinalize(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterFinalize) this.currentStep++; + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 3e25b6f5a..68d197208 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -39,6 +39,7 @@ internal class SelfTestWindow : Window new ChatAgingStep(), new HoverAgingStep(), new LuminaAgingStep(), + new AddonLifecycleAgingStep(), new PartyFinderAgingStep(), new HandledExceptionAgingStep(), new DutyStateAgingStep(), From 385c4b7a8b01b01514a1e47042d58d752f1ab147 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 13:19:44 -0700 Subject: [PATCH 072/122] Add IFramework (#1286) --- Dalamud/Game/Framework.cs | 106 ++++-------------- Dalamud/Plugin/Services/IFramework.cs | 126 ++++++++++++++++++++++ Dalamud/Utility/EventHandlerExtensions.cs | 5 +- 3 files changed, 150 insertions(+), 87 deletions(-) create mode 100644 Dalamud/Plugin/Services/IFramework.cs diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index b3083e913..2b77bf400 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -12,6 +12,7 @@ using Dalamud.Game.Gui.Toast; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -23,7 +24,10 @@ namespace Dalamud.Game; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class Framework : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed class Framework : IDisposable, IServiceType, IFramework { private static readonly Stopwatch StatsStopwatch = new(); @@ -57,12 +61,6 @@ public sealed class Framework : IDisposable, IServiceType this.destroyHook = Hook.FromAddress(this.Address.DestroyAddress, this.HandleFrameworkDestroy); } - /// - /// A delegate type used with the event. - /// - /// The Framework instance. - public delegate void OnUpdateDelegate(Framework framework); - /// /// A delegate type used during the native Framework::destroy. /// @@ -81,10 +79,8 @@ public sealed class Framework : IDisposable, IServiceType private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate - /// - /// Event that gets fired every time the game framework updates. - /// - public event OnUpdateDelegate Update; + /// + public event IFramework.OnUpdateDelegate Update; /// /// Gets or sets a value indicating whether the collection of stats is enabled. @@ -96,34 +92,22 @@ public sealed class Framework : IDisposable, IServiceType /// public static Dictionary> StatsHistory { get; } = new(); - /// - /// Gets a raw pointer to the instance of Client::Framework. - /// + /// public FrameworkAddressResolver Address { get; } - /// - /// Gets the last time that the Framework Update event was triggered. - /// + /// public DateTime LastUpdate { get; private set; } = DateTime.MinValue; - /// - /// Gets the last time in UTC that the Framework Update event was triggered. - /// + /// public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue; - /// - /// Gets the delta between the last Framework Update and the currently executing one. - /// + /// public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; - /// - /// Gets a value indicating whether currently executing code is running in the game's framework update thread. - /// + /// public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread; - /// - /// Gets a value indicating whether game Framework is unloading. - /// + /// public bool IsFrameworkUnloading { get; internal set; } /// @@ -131,20 +115,11 @@ public sealed class Framework : IDisposable, IServiceType /// internal bool DispatchUpdateEvents { get; set; } = true; - /// - /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. - /// - /// Return type. - /// Function to call. - /// Task representing the pending or already completed function. + /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func); - /// - /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. - /// - /// Function to call. - /// Task representing the pending or already completed function. + /// public Task RunOnFrameworkThread(Action action) { if (this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading) @@ -165,32 +140,15 @@ public sealed class Framework : IDisposable, IServiceType } } - /// - /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. - /// - /// Return type. - /// Function to call. - /// Task representing the pending or already completed function. + /// public Task RunOnFrameworkThread(Func> func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func); - /// - /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. - /// - /// Function to call. - /// Task representing the pending or already completed function. + /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func); - /// - /// Run given function in upcoming Framework.Tick call. - /// - /// Return type. - /// Function to call. - /// Wait for given timespan before calling this function. - /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. - /// Cancellation token which will prevent the execution of this function if wait conditions are not met. - /// Task representing the pending function. + /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) @@ -219,14 +177,7 @@ public sealed class Framework : IDisposable, IServiceType return tcs.Task; } - /// - /// Run given function in upcoming Framework.Tick call. - /// - /// Function to call. - /// Wait for given timespan before calling this function. - /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. - /// Cancellation token which will prevent the execution of this function if wait conditions are not met. - /// Task representing the pending function. + /// public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) @@ -255,15 +206,7 @@ public sealed class Framework : IDisposable, IServiceType return tcs.Task; } - /// - /// Run given function in upcoming Framework.Tick call. - /// - /// Return type. - /// Function to call. - /// Wait for given timespan before calling this function. - /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. - /// Cancellation token which will prevent the execution of this function if wait conditions are not met. - /// Task representing the pending function. + /// public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) @@ -292,14 +235,7 @@ public sealed class Framework : IDisposable, IServiceType return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap(); } - /// - /// Run given function in upcoming Framework.Tick call. - /// - /// Function to call. - /// Wait for given timespan before calling this function. - /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. - /// Cancellation token which will prevent the execution of this function if wait conditions are not met. - /// Task representing the pending function. + /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs new file mode 100644 index 000000000..69c21bca4 --- /dev/null +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -0,0 +1,126 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Game; + +namespace Dalamud.Plugin.Services; + +/// +/// This class represents the Framework of the native game client and grants access to various subsystems. +/// +public interface IFramework +{ + /// + /// A delegate type used with the event. + /// + /// The Framework instance. + public delegate void OnUpdateDelegate(Framework framework); + + /// + /// Event that gets fired every time the game framework updates. + /// + public event OnUpdateDelegate Update; + + /// + /// Gets a raw pointer to the instance of Client::Framework. + /// + public FrameworkAddressResolver Address { get; } + + /// + /// Gets the last time that the Framework Update event was triggered. + /// + public DateTime LastUpdate { get; } + + /// + /// Gets the last time in UTC that the Framework Update event was triggered. + /// + public DateTime LastUpdateUTC { get; } + + /// + /// Gets the delta between the last Framework Update and the currently executing one. + /// + public TimeSpan UpdateDelta { get; } + + /// + /// Gets a value indicating whether currently executing code is running in the game's framework update thread. + /// + public bool IsInFrameworkUpdateThread { get; } + + /// + /// Gets a value indicating whether game Framework is unloading. + /// + public bool IsFrameworkUnloading { get; } + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func func); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Action action); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func> func); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func func); + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); +} diff --git a/Dalamud/Utility/EventHandlerExtensions.cs b/Dalamud/Utility/EventHandlerExtensions.cs index bce815a7b..eefd245bb 100644 --- a/Dalamud/Utility/EventHandlerExtensions.cs +++ b/Dalamud/Utility/EventHandlerExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using Dalamud.Game; +using Dalamud.Plugin.Services; using Serilog; using static Dalamud.Game.Framework; @@ -72,12 +73,12 @@ internal static class EventHandlerExtensions /// /// The OnUpdateDelegate in question. /// Framework to be passed on to OnUpdateDelegate. - public static void InvokeSafely(this OnUpdateDelegate updateDelegate, Framework framework) + public static void InvokeSafely(this IFramework.OnUpdateDelegate updateDelegate, Framework framework) { if (updateDelegate == null) return; - foreach (var action in updateDelegate.GetInvocationList().Cast()) + foreach (var action in updateDelegate.GetInvocationList().Cast()) { HandleInvoke(() => action(framework)); } From 275ec72ab769cfcbf85c09d2c693254219a3c8ad Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 10 Sep 2023 14:56:28 -0700 Subject: [PATCH 073/122] Re-Add Lost Import - No idea how, but the import for ImGuiHelpers was lost. --- Dalamud/Interface/Components/ImGuiComponents.IconButton.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs index 05e660b61..1c484d423 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs @@ -1,6 +1,7 @@ using System; using System.Numerics; +using Dalamud.Interface.Utility; using ImGuiNET; namespace Dalamud.Interface.Components; From d378fe1dfcb0255eaa03d0818d7025ce4cbaeb26 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 15:20:44 -0700 Subject: [PATCH 074/122] Add IPartyFinderGui (v9) (#1279) --- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 52 +++++++++++++------ Dalamud/Plugin/Services/IPartyFinderGui.cs | 23 ++++++++ 2 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 Dalamud/Plugin/Services/IPartyFinderGui.cs diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 6427f2a54..85c6a4a39 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -6,6 +6,7 @@ using Dalamud.Game.Gui.PartyFinder.Types; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.Gui.PartyFinder; @@ -13,10 +14,9 @@ namespace Dalamud.Game.Gui.PartyFinder; /// /// This class handles interacting with the native PartyFinder window. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class PartyFinderGui : IDisposable, IServiceType +internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGui { private readonly PartyFinderAddressResolver address; private readonly IntPtr memory; @@ -35,25 +35,14 @@ public sealed class PartyFinderGui : IDisposable, IServiceType this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize); - this.receiveListingHook = Hook.FromAddress(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour)); + this.receiveListingHook = Hook.FromAddress(this.address.ReceiveListing, this.HandleReceiveListingDetour); } - /// - /// Event type fired each time the game receives an individual Party Finder listing. - /// Cannot modify listings but can hide them. - /// - /// The listings received. - /// Additional arguments passed by the game. - public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data); - /// - /// Event fired each time the game receives an individual Party Finder listing. - /// Cannot modify listings but can hide them. - /// - public event PartyFinderListingEventDelegate ReceiveListing; + /// + public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing; /// /// Dispose of managed and unmanaged resources. @@ -138,3 +127,34 @@ public sealed class PartyFinderGui : IDisposable, IServiceType } } } + +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFinderGui +{ + [ServiceManager.ServiceDependency] + private readonly PartyFinderGui partyFinderGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal PartyFinderGuiPluginScoped() + { + this.partyFinderGuiService.ReceiveListing += this.ReceiveListingForward; + } + + /// + public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing; + + /// + public void Dispose() + { + this.partyFinderGuiService.ReceiveListing -= this.ReceiveListingForward; + } + + private void ReceiveListingForward(PartyFinderListing listing, PartyFinderListingEventArgs args) => this.ReceiveListing?.Invoke(listing, args); +} diff --git a/Dalamud/Plugin/Services/IPartyFinderGui.cs b/Dalamud/Plugin/Services/IPartyFinderGui.cs new file mode 100644 index 000000000..f656963db --- /dev/null +++ b/Dalamud/Plugin/Services/IPartyFinderGui.cs @@ -0,0 +1,23 @@ +using Dalamud.Game.Gui.PartyFinder.Types; + +namespace Dalamud.Plugin.Services; + +/// +/// This class handles interacting with the native PartyFinder window. +/// +public interface IPartyFinderGui +{ + /// + /// Event type fired each time the game receives an individual Party Finder listing. + /// Cannot modify listings but can hide them. + /// + /// The listings received. + /// Additional arguments passed by the game. + public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args); + + /// + /// Event fired each time the game receives an individual Party Finder listing. + /// Cannot modify listings but can hide them. + /// + public event PartyFinderListingEventDelegate ReceiveListing; +} From 617c2bdb9c752ee067eacbabf5827dc2731b06e5 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 16:18:10 -0700 Subject: [PATCH 075/122] Add IFlyTextGui (v9) (#1278) --- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 92 +++++++++++++------------- Dalamud/Plugin/Services/IFlyTextGui.cs | 55 +++++++++++++++ 2 files changed, 101 insertions(+), 46 deletions(-) create mode 100644 Dalamud/Plugin/Services/IFlyTextGui.cs diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index f2222a7cd..3c04c744a 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -7,6 +7,7 @@ using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Memory; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.Gui.FlyText; @@ -14,10 +15,9 @@ namespace Dalamud.Game.Gui.FlyText; /// /// This class facilitates interacting with and creating native in-game "fly text". /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class FlyTextGui : IDisposable, IServiceType +internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui { /// /// The native function responsible for adding fly text to the UI. See . @@ -39,32 +39,6 @@ public sealed class FlyTextGui : IDisposable, IServiceType this.createFlyTextHook = Hook.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour); } - /// - /// The delegate defining the type for the FlyText event. - /// - /// The FlyTextKind. See . - /// Value1 passed to the native flytext function. - /// Value2 passed to the native flytext function. Seems unused. - /// Text1 passed to the native flytext function. - /// Text2 passed to the native flytext function. - /// Color passed to the native flytext function. Changes flytext color. - /// Icon ID passed to the native flytext function. Only displays with select FlyTextKind. - /// Damage Type Icon ID passed to the native flytext function. Displayed next to damage values to denote damage type. - /// The vertical offset to place the flytext at. 0 is default. Negative values result - /// in text appearing higher on the screen. This does not change where the element begins to fade. - /// Whether this flytext has been handled. If a subscriber sets this to true, the FlyText will not appear. - public delegate void OnFlyTextCreatedDelegate( - ref FlyTextKind kind, - ref int val1, - ref int val2, - ref SeString text1, - ref SeString text2, - ref uint color, - ref uint icon, - ref uint damageTypeIcon, - ref float yOffset, - ref bool handled); - /// /// Private delegate for the native CreateFlyText function's hook. /// @@ -95,12 +69,8 @@ public sealed class FlyTextGui : IDisposable, IServiceType uint offsetStrMax, int unknown); - /// - /// The FlyText event that can be subscribed to. - /// - public event OnFlyTextCreatedDelegate? FlyTextCreated; - - private Dalamud Dalamud { get; } + /// + public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated; private FlyTextGuiAddressResolver Address { get; } @@ -112,18 +82,7 @@ public sealed class FlyTextGui : IDisposable, IServiceType this.createFlyTextHook.Dispose(); } - /// - /// Displays a fly text in-game on the local player. - /// - /// The FlyTextKind. See . - /// The index of the actor to place flytext on. Indexing unknown. 1 places flytext on local player. - /// Value1 passed to the native flytext function. - /// Value2 passed to the native flytext function. Seems unused. - /// Text1 passed to the native flytext function. - /// Text2 passed to the native flytext function. - /// Color passed to the native flytext function. Changes flytext color. - /// Icon ID passed to the native flytext function. Only displays with select FlyTextKind. - /// Damage Type Icon ID passed to the native flytext function. Displayed next to damage values to denote damage type. + /// public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon) { // Known valid flytext region within the atk arrays @@ -318,3 +277,44 @@ public sealed class FlyTextGui : IDisposable, IServiceType return retVal; } } + +/// +/// Plugin scoped version of FlyTextGui. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui +{ + [ServiceManager.ServiceDependency] + private readonly FlyTextGui flyTextGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal FlyTextGuiPluginScoped() + { + this.flyTextGuiService.FlyTextCreated += this.FlyTextCreatedForward; + } + + /// + public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated; + + /// + public void Dispose() + { + this.flyTextGuiService.FlyTextCreated -= this.FlyTextCreatedForward; + } + + /// + public void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon) + { + this.flyTextGuiService.AddFlyText(kind, actorIndex, val1, val2, text1, text2, color, icon, damageTypeIcon); + } + + private void FlyTextCreatedForward(ref FlyTextKind kind, ref int val1, ref int val2, ref SeString text1, ref SeString text2, ref uint color, ref uint icon, ref uint damageTypeIcon, ref float yOffset, ref bool handled) + => this.FlyTextCreated?.Invoke(ref kind, ref val1, ref val2, ref text1, ref text2, ref color, ref icon, ref damageTypeIcon, ref yOffset, ref handled); +} diff --git a/Dalamud/Plugin/Services/IFlyTextGui.cs b/Dalamud/Plugin/Services/IFlyTextGui.cs new file mode 100644 index 000000000..04fae351d --- /dev/null +++ b/Dalamud/Plugin/Services/IFlyTextGui.cs @@ -0,0 +1,55 @@ +using Dalamud.Game.Gui.FlyText; +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Plugin.Services; + +/// +/// This class facilitates interacting with and creating native in-game "fly text". +/// +public interface IFlyTextGui +{ + /// + /// The delegate defining the type for the FlyText event. + /// + /// The FlyTextKind. See . + /// Value1 passed to the native flytext function. + /// Value2 passed to the native flytext function. Seems unused. + /// Text1 passed to the native flytext function. + /// Text2 passed to the native flytext function. + /// Color passed to the native flytext function. Changes flytext color. + /// Icon ID passed to the native flytext function. Only displays with select FlyTextKind. + /// Damage Type Icon ID passed to the native flytext function. Displayed next to damage values to denote damage type. + /// The vertical offset to place the flytext at. 0 is default. Negative values result + /// in text appearing higher on the screen. This does not change where the element begins to fade. + /// Whether this flytext has been handled. If a subscriber sets this to true, the FlyText will not appear. + public delegate void OnFlyTextCreatedDelegate( + ref FlyTextKind kind, + ref int val1, + ref int val2, + ref SeString text1, + ref SeString text2, + ref uint color, + ref uint icon, + ref uint damageTypeIcon, + ref float yOffset, + ref bool handled); + + /// + /// The FlyText event that can be subscribed to. + /// + public event OnFlyTextCreatedDelegate? FlyTextCreated; + + /// + /// Displays a fly text in-game on the local player. + /// + /// The FlyTextKind. See . + /// The index of the actor to place flytext on. Indexing unknown. 1 places flytext on local player. + /// Value1 passed to the native flytext function. + /// Value2 passed to the native flytext function. Seems unused. + /// Text1 passed to the native flytext function. + /// Text2 passed to the native flytext function. + /// Color passed to the native flytext function. Changes flytext color. + /// Icon ID passed to the native flytext function. Only displays with select FlyTextKind. + /// Damage Type Icon ID passed to the native flytext function. Displayed next to damage values to denote damage type. + public void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon); +} From 0f3b9eab8cd52d0e42a269f3f4d32caefaca7824 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 16:24:47 -0700 Subject: [PATCH 076/122] Add IToastGui (v9) (#1280) --- Dalamud/Game/Gui/Toast/ToastGui.cs | 183 ++++++++++++++------------- Dalamud/Plugin/Services/IToastGui.cs | 88 +++++++++++++ 2 files changed, 186 insertions(+), 85 deletions(-) create mode 100644 Dalamud/Plugin/Services/IToastGui.cs diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index e65fa1444..93126710b 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Text; @@ -6,16 +5,16 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; namespace Dalamud.Game.Gui.Toast; /// /// This class facilitates interacting with and creating native toast windows. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed partial class ToastGui : IDisposable, IServiceType +internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui { private const uint QuestToastCheckmarkMagic = 60081; @@ -39,38 +38,11 @@ public sealed partial class ToastGui : IDisposable, IServiceType this.address = new ToastGuiAddressResolver(); this.address.Setup(sigScanner); - this.showNormalToastHook = Hook.FromAddress(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour)); - this.showQuestToastHook = Hook.FromAddress(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour)); - this.showErrorToastHook = Hook.FromAddress(this.address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour)); + this.showNormalToastHook = Hook.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour); + this.showQuestToastHook = Hook.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour); + this.showErrorToastHook = Hook.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour); } - #region Event delegates - - /// - /// A delegate type used when a normal toast window appears. - /// - /// The message displayed. - /// Assorted toast options. - /// Whether the toast has been handled or should be propagated. - public delegate void OnNormalToastDelegate(ref SeString message, ref ToastOptions options, ref bool isHandled); - - /// - /// A delegate type used when a quest toast window appears. - /// - /// The message displayed. - /// Assorted toast options. - /// Whether the toast has been handled or should be propagated. - public delegate void OnQuestToastDelegate(ref SeString message, ref QuestToastOptions options, ref bool isHandled); - - /// - /// A delegate type used when an error toast window appears. - /// - /// The message displayed. - /// Whether the toast has been handled or should be propagated. - public delegate void OnErrorToastDelegate(ref SeString message, ref bool isHandled); - - #endregion - #region Marshal delegates private delegate IntPtr ShowNormalToastDelegate(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId); @@ -82,21 +54,15 @@ public sealed partial class ToastGui : IDisposable, IServiceType #endregion #region Events + + /// + public event IToastGui.OnNormalToastDelegate? Toast; - /// - /// Event that will be fired when a toast is sent by the game or a plugin. - /// - public event OnNormalToastDelegate Toast; + /// + public event IToastGui.OnQuestToastDelegate? QuestToast; - /// - /// Event that will be fired when a quest toast is sent by the game or a plugin. - /// - public event OnQuestToastDelegate QuestToast; - - /// - /// Event that will be fired when an error toast is sent by the game or a plugin. - /// - public event OnErrorToastDelegate ErrorToast; + /// + public event IToastGui.OnErrorToastDelegate? ErrorToast; #endregion @@ -172,31 +138,23 @@ public sealed partial class ToastGui : IDisposable, IServiceType /// /// Handles normal toasts. /// -public sealed partial class ToastGui +internal sealed partial class ToastGui { - /// - /// Show a toast message with the given content. - /// - /// The message to be shown. - /// Options for the toast. - public void ShowNormal(string message, ToastOptions options = null) + /// + public void ShowNormal(string message, ToastOptions? options = null) { options ??= new ToastOptions(); this.normalQueue.Enqueue((Encoding.UTF8.GetBytes(message), options)); } - - /// - /// Show a toast message with the given content. - /// - /// The message to be shown. - /// Options for the toast. - public void ShowNormal(SeString message, ToastOptions options = null) + + /// + public void ShowNormal(SeString message, ToastOptions? options = null) { options ??= new ToastOptions(); this.normalQueue.Enqueue((message.Encode(), options)); } - private void ShowNormal(byte[] bytes, ToastOptions options = null) + private void ShowNormal(byte[] bytes, ToastOptions? options = null) { options ??= new ToastOptions(); @@ -255,31 +213,23 @@ public sealed partial class ToastGui /// /// Handles quest toasts. /// -public sealed partial class ToastGui +internal sealed partial class ToastGui { - /// - /// Show a quest toast message with the given content. - /// - /// The message to be shown. - /// Options for the toast. - public void ShowQuest(string message, QuestToastOptions options = null) + /// + public void ShowQuest(string message, QuestToastOptions? options = null) { options ??= new QuestToastOptions(); this.questQueue.Enqueue((Encoding.UTF8.GetBytes(message), options)); } - - /// - /// Show a quest toast message with the given content. - /// - /// The message to be shown. - /// Options for the toast. - public void ShowQuest(SeString message, QuestToastOptions options = null) + + /// + public void ShowQuest(SeString message, QuestToastOptions? options = null) { options ??= new QuestToastOptions(); this.questQueue.Enqueue((message.Encode(), options)); } - private void ShowQuest(byte[] bytes, QuestToastOptions options = null) + private void ShowQuest(byte[] bytes, QuestToastOptions? options = null) { options ??= new QuestToastOptions(); @@ -365,21 +315,15 @@ public sealed partial class ToastGui /// /// Handles error toasts. /// -public sealed partial class ToastGui +internal sealed partial class ToastGui { - /// - /// Show an error toast message with the given content. - /// - /// The message to be shown. + /// public void ShowError(string message) { this.errorQueue.Enqueue(Encoding.UTF8.GetBytes(message)); } - /// - /// Show an error toast message with the given content. - /// - /// The message to be shown. + /// public void ShowError(SeString message) { this.errorQueue.Enqueue(message.Encode()); @@ -433,3 +377,72 @@ public sealed partial class ToastGui } } } + +/// +/// Plugin scoped version of ToastGui. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui +{ + [ServiceManager.ServiceDependency] + private readonly ToastGui toastGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal ToastGuiPluginScoped() + { + this.toastGuiService.Toast += this.ToastForward; + this.toastGuiService.QuestToast += this.QuestToastForward; + this.toastGuiService.ErrorToast += this.ErrorToastForward; + } + + /// + public event IToastGui.OnNormalToastDelegate? Toast; + + /// + public event IToastGui.OnQuestToastDelegate? QuestToast; + + /// + public event IToastGui.OnErrorToastDelegate? ErrorToast; + + /// + public void Dispose() + { + this.toastGuiService.Toast -= this.ToastForward; + this.toastGuiService.QuestToast -= this.QuestToastForward; + this.toastGuiService.ErrorToast -= this.ErrorToastForward; + } + + /// + public void ShowNormal(string message, ToastOptions? options = null) => this.toastGuiService.ShowNormal(message, options); + + /// + public void ShowNormal(SeString message, ToastOptions? options = null) => this.toastGuiService.ShowNormal(message, options); + + /// + public void ShowQuest(string message, QuestToastOptions? options = null) => this.toastGuiService.ShowQuest(message, options); + + /// + public void ShowQuest(SeString message, QuestToastOptions? options = null) => this.toastGuiService.ShowQuest(message, options); + + /// + public void ShowError(string message) => this.toastGuiService.ShowError(message); + + /// + public void ShowError(SeString message) => this.toastGuiService.ShowError(message); + + private void ToastForward(ref SeString message, ref ToastOptions options, ref bool isHandled) + => this.Toast?.Invoke(ref message, ref options, ref isHandled); + + private void QuestToastForward(ref SeString message, ref QuestToastOptions options, ref bool isHandled) + => this.QuestToast?.Invoke(ref message, ref options, ref isHandled); + + private void ErrorToastForward(ref SeString message, ref bool isHandled) + => this.ErrorToast?.Invoke(ref message, ref isHandled); +} diff --git a/Dalamud/Plugin/Services/IToastGui.cs b/Dalamud/Plugin/Services/IToastGui.cs new file mode 100644 index 000000000..ef83e95ac --- /dev/null +++ b/Dalamud/Plugin/Services/IToastGui.cs @@ -0,0 +1,88 @@ +using Dalamud.Game.Gui.Toast; +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Plugin.Services; + +/// +/// This class facilitates interacting with and creating native toast windows. +/// +public interface IToastGui +{ + /// + /// A delegate type used when a normal toast window appears. + /// + /// The message displayed. + /// Assorted toast options. + /// Whether the toast has been handled or should be propagated. + public delegate void OnNormalToastDelegate(ref SeString message, ref ToastOptions options, ref bool isHandled); + + /// + /// A delegate type used when a quest toast window appears. + /// + /// The message displayed. + /// Assorted toast options. + /// Whether the toast has been handled or should be propagated. + public delegate void OnQuestToastDelegate(ref SeString message, ref QuestToastOptions options, ref bool isHandled); + + /// + /// A delegate type used when an error toast window appears. + /// + /// The message displayed. + /// Whether the toast has been handled or should be propagated. + public delegate void OnErrorToastDelegate(ref SeString message, ref bool isHandled); + + /// + /// Event that will be fired when a toast is sent by the game or a plugin. + /// + public event OnNormalToastDelegate Toast; + + /// + /// Event that will be fired when a quest toast is sent by the game or a plugin. + /// + public event OnQuestToastDelegate QuestToast; + + /// + /// Event that will be fired when an error toast is sent by the game or a plugin. + /// + public event OnErrorToastDelegate ErrorToast; + + /// + /// Show a toast message with the given content. + /// + /// The message to be shown. + /// Options for the toast. + public void ShowNormal(string message, ToastOptions? options = null); + + /// + /// Show a toast message with the given content. + /// + /// The message to be shown. + /// Options for the toast. + public void ShowNormal(SeString message, ToastOptions? options = null); + + /// + /// Show a quest toast message with the given content. + /// + /// The message to be shown. + /// Options for the toast. + public void ShowQuest(string message, QuestToastOptions? options = null); + + /// + /// Show a quest toast message with the given content. + /// + /// The message to be shown. + /// Options for the toast. + public void ShowQuest(SeString message, QuestToastOptions? options = null); + + /// + /// Show an error toast message with the given content. + /// + /// The message to be shown. + public void ShowError(string message); + + /// + /// Show an error toast message with the given content. + /// + /// The message to be shown. + public void ShowError(SeString message); +} From ca58a1bf4fa4852b6ea77a1f09088e57439c9842 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 19:26:28 -0700 Subject: [PATCH 077/122] Move AddonArgs to it's own file --- Dalamud/Game/AddonLifecycle/AddonArgs.cs | 22 +++++++++++++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 24 +++++++++---------- Dalamud/Plugin/Services/IAddonLifecycle.cs | 20 ---------------- 3 files changed, 34 insertions(+), 32 deletions(-) create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgs.cs diff --git a/Dalamud/Game/AddonLifecycle/AddonArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgs.cs new file mode 100644 index 000000000..50c995abb --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgs.cs @@ -0,0 +1,22 @@ +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonLifecycle; + +/// +/// 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.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + required public nint Addon { get; init; } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index c0e817f0d..5fc1c7d2b 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -127,7 +127,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRequestedUpdateHook.Enable(); } - private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) + private void InvokeListeners(AddonEvent eventType, AddonArgs args) { // Match on string.empty for listeners that want events for all addons. foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) @@ -140,7 +140,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreSetup, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -151,7 +151,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostSetup, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -165,7 +165,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreFinalize, new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + this.InvokeListeners(AddonEvent.PreFinalize, new AddonArgs { Addon = (nint)atkUnitBase[0] }); } catch (Exception e) { @@ -179,7 +179,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreDraw, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -190,7 +190,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostDraw, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -202,7 +202,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreUpdate, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -213,7 +213,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostUpdate, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -225,7 +225,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreRefresh, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreRefresh, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -236,7 +236,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostRefresh, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostRefresh, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -248,7 +248,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreRequestedUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -259,7 +259,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostRequestedUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index cbb3d7c24..1dc792660 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -2,8 +2,6 @@ using System.Runtime.InteropServices; using Dalamud.Game.AddonLifecycle; -using Dalamud.Memory; -using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Plugin.Services; @@ -79,22 +77,4 @@ public interface IAddonLifecycle /// /// Handlers to remove. void UnregisterListener(params AddonEventDelegate[] handlers); - - /// - /// 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.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); - - /// - /// Gets the pointer to the addons AtkUnitBase. - /// - required public nint Addon { get; init; } - } } From 9c0ca2769c774c02a0d510ba36d953e609d93888 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 20:01:49 -0700 Subject: [PATCH 078/122] Fix Aging Steps --- .../SelfTest/AgingSteps/AddonLifecycleAgingStep.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs index 9dcaec558..3a1cb0e77 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Dalamud.Game.AddonLifecycle; -using Dalamud.Plugin.Services; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; @@ -101,32 +100,32 @@ internal class AddonLifecycleAgingStep : IAgingStep } } - private void PostSetup(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostSetup(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterSetup) this.currentStep++; } - private void PostUpdate(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostUpdate(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterUpdate) this.currentStep++; } - private void PostDraw(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostDraw(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterDraw) this.currentStep++; } - private void PostRefresh(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostRefresh(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterRefresh) this.currentStep++; } - private void PostRequestedUpdate(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostRequestedUpdate(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterRequestedUpdate) this.currentStep++; } - private void PreFinalize(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PreFinalize(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterFinalize) this.currentStep++; } From 4870428bac6c90bd498e11796f53c358967df551 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 11 Sep 2023 21:32:56 -0700 Subject: [PATCH 079/122] Bump Lumina to 3.11.0 --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 2 +- Dalamud/Dalamud.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 25919af07..3938d0c80 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,7 +27,7 @@ - + diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index b147dc961..98b515642 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -67,7 +67,7 @@ - + From 130a57f850660992c12ca426cc0510f0b7f253f8 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 12 Sep 2023 07:02:32 +0200 Subject: [PATCH 080/122] Update ClientStructs (#1371) 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 ada62e7ae..7279a8f3c 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit ada62e7ae60de220d1f950b03ddb8d66e9e10daf +Subproject commit 7279a8f3ca6b79490184b05532af509781a89415 From af632175647debf7d469ba2f709ffe1e186e0e10 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:06:40 -0700 Subject: [PATCH 081/122] Add ITitleScreenMenu and Scoped Service. (#1379) --- .../Internal/Windows/TitleScreenMenuWindow.cs | 3 +- .../{ => TitleScreenMenu}/TitleScreenMenu.cs | 168 ++++++------------ .../TitleScreenMenu/TitleScreenMenuEntry.cs | 94 ++++++++++ Dalamud/Plugin/Services/ITitleScreenMenu.cs | 44 +++++ 4 files changed, 195 insertions(+), 114 deletions(-) rename Dalamud/Interface/{ => TitleScreenMenu}/TitleScreenMenu.cs (50%) create mode 100644 Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs create mode 100644 Dalamud/Plugin/Services/ITitleScreenMenu.cs diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index e92c80846..e3cf78296 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -230,7 +229,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable } private bool DrawEntry( - TitleScreenMenu.TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) + TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) { InterfaceManager.SpecialGlyphRequest fontHandle; if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) diff --git a/Dalamud/Interface/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs similarity index 50% rename from Dalamud/Interface/TitleScreenMenu.cs rename to Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs index c9e1458d6..3123ffbb8 100644 --- a/Dalamud/Interface/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using ImGuiScene; namespace Dalamud.Interface; @@ -12,10 +12,9 @@ namespace Dalamud.Interface; /// /// Class responsible for managing elements in the title screen menu. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public class TitleScreenMenu : IServiceType +internal class TitleScreenMenu : IServiceType, ITitleScreenMenu { /// /// Gets the texture size needed for title screen menu logos. @@ -29,19 +28,10 @@ public class TitleScreenMenu : IServiceType { } - /// - /// Gets the list of entries in the title screen menu. - /// + /// public IReadOnlyList Entries => this.entries; - /// - /// Adds a new entry to the title screen menu. - /// - /// The text to show. - /// The texture to show. - /// The action to execute when the option is selected. - /// A object that can be used to manage the entry. - /// Thrown when the texture provided does not match the required resolution(64x64). + /// public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered) { if (texture.Height != TextureSize || texture.Width != TextureSize) @@ -64,15 +54,7 @@ public class TitleScreenMenu : IServiceType } } - /// - /// Adds a new entry to the title screen menu. - /// - /// Priority of the entry. - /// The text to show. - /// The texture to show. - /// The action to execute when the option is selected. - /// A object that can be used to manage the entry. - /// Thrown when the texture provided does not match the required resolution(64x64). + /// public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered) { if (texture.Height != TextureSize || texture.Width != TextureSize) @@ -91,10 +73,7 @@ public class TitleScreenMenu : IServiceType } } - /// - /// Remove an entry from the title screen menu. - /// - /// The entry to remove. + /// public void RemoveEntry(TitleScreenMenuEntry entry) { lock (this.entries) @@ -159,93 +138,58 @@ public class TitleScreenMenu : IServiceType return entry; } } +} - /// - /// Class representing an entry in the title screen menu. - /// - public class TitleScreenMenuEntry : IComparable +/// +/// Plugin-scoped version of a TitleScreenMenu service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleScreenMenu +{ + [ServiceManager.ServiceDependency] + private readonly TitleScreenMenu titleScreenMenuService = Service.Get(); + + private readonly List pluginEntries = new(); + + /// + public IReadOnlyList? Entries => this.titleScreenMenuService.Entries; + + /// + public void Dispose() { - private readonly Action onTriggered; - - /// - /// Initializes a new instance of the class. - /// - /// The calling assembly. - /// The priority of this entry. - /// The text to show. - /// The texture to show. - /// The action to execute when the option is selected. - internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, TextureWrap texture, Action onTriggered) + foreach (var entry in this.pluginEntries) { - this.CallingAssembly = callingAssembly; - this.Priority = priority; - this.Name = text; - this.Texture = texture; - this.onTriggered = onTriggered; - } - - /// - /// Gets the priority of this entry. - /// - public ulong Priority { get; init; } - - /// - /// Gets or sets the name of this entry. - /// - public string Name { get; set; } - - /// - /// 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. - /// - internal Assembly? CallingAssembly { get; init; } - - /// - /// Gets the internal ID of this entry. - /// - internal Guid Id { get; init; } = Guid.NewGuid(); - - /// - public int CompareTo(TitleScreenMenuEntry? other) - { - if (other == null) - return 1; - if (this.CallingAssembly != other.CallingAssembly) - { - if (this.CallingAssembly == null && other.CallingAssembly == null) - return 0; - if (this.CallingAssembly == null && other.CallingAssembly != null) - return -1; - if (this.CallingAssembly != null && other.CallingAssembly == null) - return 1; - return string.Compare( - this.CallingAssembly!.FullName!, - other.CallingAssembly!.FullName!, - StringComparison.CurrentCultureIgnoreCase); - } - - if (this.Priority != other.Priority) - return this.Priority.CompareTo(other.Priority); - if (this.Name != other.Name) - return string.Compare(this.Name, other.Name, StringComparison.InvariantCultureIgnoreCase); - return string.Compare(this.Name, other.Name, StringComparison.InvariantCulture); - } - - /// - /// Trigger the action associated with this entry. - /// - internal void Trigger() - { - this.onTriggered(); + this.titleScreenMenuService.RemoveEntry(entry); } } + + /// + public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered) + { + var entry = this.titleScreenMenuService.AddEntry(text, texture, onTriggered); + this.pluginEntries.Add(entry); + + return entry; + } + + /// + public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered) + { + var entry = this.titleScreenMenuService.AddEntry(priority, text, texture, onTriggered); + this.pluginEntries.Add(entry); + + return entry; + } + + /// + public void RemoveEntry(TitleScreenMenuEntry entry) + { + this.pluginEntries.Remove(entry); + this.titleScreenMenuService.RemoveEntry(entry); + } } diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs new file mode 100644 index 000000000..18acc4f47 --- /dev/null +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs @@ -0,0 +1,94 @@ +using System.Reflection; + +using ImGuiScene; + +namespace Dalamud.Interface; + +/// +/// Class representing an entry in the title screen menu. +/// +public class TitleScreenMenuEntry : IComparable +{ + private readonly Action onTriggered; + + /// + /// Initializes a new instance of the class. + /// + /// The calling assembly. + /// The priority of this entry. + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, TextureWrap texture, Action onTriggered) + { + this.CallingAssembly = callingAssembly; + this.Priority = priority; + this.Name = text; + this.Texture = texture; + this.onTriggered = onTriggered; + } + + /// + /// Gets the priority of this entry. + /// + public ulong Priority { get; init; } + + /// + /// Gets or sets the name of this entry. + /// + public string Name { get; set; } + + /// + /// 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. + /// + internal Assembly? CallingAssembly { get; init; } + + /// + /// Gets the internal ID of this entry. + /// + internal Guid Id { get; init; } = Guid.NewGuid(); + + /// + public int CompareTo(TitleScreenMenuEntry? other) + { + if (other == null) + return 1; + if (this.CallingAssembly != other.CallingAssembly) + { + if (this.CallingAssembly == null && other.CallingAssembly == null) + return 0; + if (this.CallingAssembly == null && other.CallingAssembly != null) + return -1; + if (this.CallingAssembly != null && other.CallingAssembly == null) + return 1; + return string.Compare( + this.CallingAssembly!.FullName!, + other.CallingAssembly!.FullName!, + StringComparison.CurrentCultureIgnoreCase); + } + + if (this.Priority != other.Priority) + return this.Priority.CompareTo(other.Priority); + if (this.Name != other.Name) + return string.Compare(this.Name, other.Name, StringComparison.InvariantCultureIgnoreCase); + return 0; + } + + /// + /// Trigger the action associated with this entry. + /// + internal void Trigger() + { + this.onTriggered(); + } +} diff --git a/Dalamud/Plugin/Services/ITitleScreenMenu.cs b/Dalamud/Plugin/Services/ITitleScreenMenu.cs new file mode 100644 index 000000000..2094dc435 --- /dev/null +++ b/Dalamud/Plugin/Services/ITitleScreenMenu.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +using Dalamud.Interface; +using ImGuiScene; + +namespace Dalamud.Plugin.Services; + +/// +/// Interface for class responsible for managing elements in the title screen menu. +/// +public interface ITitleScreenMenu +{ + /// + /// Gets the list of entries in the title screen menu. + /// + public IReadOnlyList Entries { get; } + + /// + /// Adds a new entry to the title screen menu. + /// + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + /// A object that can be used to manage the entry. + /// Thrown when the texture provided does not match the required resolution(64x64). + public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered); + + /// + /// Adds a new entry to the title screen menu. + /// + /// Priority of the entry. + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + /// A object that can be used to manage the entry. + /// Thrown when the texture provided does not match the required resolution(64x64). + public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered); + + /// + /// Remove an entry from the title screen menu. + /// + /// The entry to remove. + public void RemoveEntry(TitleScreenMenuEntry entry); +} From 181ec6b95623b6cf15c3b7063785c7976065bc4a Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:07:46 -0700 Subject: [PATCH 082/122] Add GameNetworkPluginScoped (#1380) --- Dalamud/Game/Network/GameNetwork.cs | 52 +++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 2b6630c8b..f56fd3996 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; @@ -14,13 +13,9 @@ namespace Dalamud.Game.Network; /// /// This class handles interacting with game network events. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork +internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork { private readonly GameNetworkAddressResolver address; private readonly Hook processZonePacketDownHook; @@ -57,14 +52,10 @@ public sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate byte ProcessZonePacketUpDelegate(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4); - /// - /// Event that is called when a network message is sent/received. - /// - public event IGameNetwork.OnNetworkMessageDelegate NetworkMessage; + /// + public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; - /// - /// Dispose of managed and unmanaged resources. - /// + /// void IDisposable.Dispose() { this.processZonePacketDownHook.Dispose(); @@ -148,3 +139,38 @@ public sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork return this.processZonePacketUpHook.Original(a1, dataPtr, a3, a4); } } + +/// +/// Plugin-scoped version of a AddonLifecycle service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork +{ + [ServiceManager.ServiceDependency] + private readonly GameNetwork gameNetworkService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal GameNetworkPluginScoped() + { + this.gameNetworkService.NetworkMessage += this.NetworkMessageForward; + } + + /// + public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; + + /// + public void Dispose() + { + this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward; + } + + private void NetworkMessageForward(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) + => this.NetworkMessage?.Invoke(dataPtr, opCode, sourceActorId, targetActorId, direction); +} From abf7c243e428166bcfd9b9dc71b28a3d24412c32 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:09:01 -0700 Subject: [PATCH 083/122] Add GameGuiPluginScoped (#1381) --- Dalamud/Game/Gui/GameGui.cs | 143 +++++++++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 26 deletions(-) diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 3954954a3..078c624e8 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -1,13 +1,12 @@ -using System; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; -using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; @@ -16,7 +15,6 @@ using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; -using Serilog; using SharpDX; using Vector2 = System.Numerics.Vector2; @@ -27,14 +25,12 @@ namespace Dalamud.Game.Gui; /// /// A class handling many aspects of the in-game UI. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui +internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui { + private static readonly ModuleLog Log = new("GameGui"); + private readonly GameGuiAddressResolver address; private readonly GetMatrixSingletonDelegate getMatrixSingleton; @@ -48,8 +44,8 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui private readonly Hook toggleUiHideHook; private readonly Hook utf8StringFromSequenceHook; - private GetUIMapObjectDelegate getUIMapObject; - private OpenMapWithFlagDelegate openMapWithFlag; + private GetUIMapObjectDelegate? getUIMapObject; + private OpenMapWithFlagDelegate? openMapWithFlag; [ServiceManager.ServiceConstructor] private GameGui(SigScanner sigScanner) @@ -116,16 +112,16 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui private delegate char HandleImmDelegate(IntPtr framework, char a2, byte a3); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, byte unknownByte); + private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, bool uiVisible); /// - public event EventHandler UiHideToggled; + public event EventHandler? UiHideToggled; /// - public event EventHandler HoveredItemChanged; + public event EventHandler? HoveredItemChanged; /// - public event EventHandler HoveredActionChanged; + public event EventHandler? HoveredActionChanged; /// public bool GameUiHidden { get; private set; } @@ -147,7 +143,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui return false; } - this.getUIMapObject = this.address.GetVirtualFunction(uiModule, 0, 8); + this.getUIMapObject ??= this.address.GetVirtualFunction(uiModule, 0, 8); var uiMapObjectPtr = this.getUIMapObject(uiModule); @@ -157,7 +153,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui return false; } - this.openMapWithFlag = this.address.GetVirtualFunction(uiMapObjectPtr, 0, 63); + this.openMapWithFlag ??= this.address.GetVirtualFunction(uiMapObjectPtr, 0, 63); var mapLinkString = mapLink.DataString; @@ -217,14 +213,13 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui // Read current ViewProjectionMatrix plus game window size var viewProjectionMatrix = default(Matrix); - float width, height; var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer(); for (var i = 0; i < 16; i++, rawMatrix++) viewProjectionMatrix[i] = *rawMatrix; - width = *rawMatrix; - height = *(rawMatrix + 1); + var width = *rawMatrix; + var height = *(rawMatrix + 1); viewProjectionMatrix.Invert(); @@ -414,7 +409,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui this.HoveredItemChanged?.InvokeSafely(this, itemId); - Log.Verbose("HoverItemId:{0} this:{1}", itemId, hoverState.ToInt64().ToString("X")); + Log.Verbose($"HoverItemId:{itemId} this:{hoverState.ToInt64()}"); } return retVal; @@ -456,7 +451,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui this.HoveredAction.ActionID = (uint)Marshal.ReadInt32(hoverState, 0x3C); this.HoveredActionChanged?.InvokeSafely(this, this.HoveredAction); - Log.Verbose("HoverActionId: {0}/{1} this:{2}", actionKind, actionId, hoverState.ToInt64().ToString("X")); + Log.Verbose($"HoverActionId: {actionKind}/{actionId} this:{hoverState.ToInt64():X}"); } private IntPtr HandleActionOutDetour(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4) @@ -489,16 +484,15 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui return retVal; } - private IntPtr ToggleUiHideDetour(IntPtr thisPtr, byte unknownByte) + private IntPtr ToggleUiHideDetour(IntPtr thisPtr, bool uiVisible) { - // TODO(goat): We should read this from memory directly, instead of relying on catching every toggle. - this.GameUiHidden = !this.GameUiHidden; + this.GameUiHidden = !RaptureAtkModule.Instance()->IsUiVisible; this.UiHideToggled?.InvokeSafely(this, this.GameUiHidden); Log.Debug("UiHide toggled: {0}", this.GameUiHidden); - return this.toggleUiHideHook.Original(thisPtr, unknownByte); + return this.toggleUiHideHook.Original(thisPtr, uiVisible); } private char HandleImmDetour(IntPtr framework, char a2, byte a3) @@ -514,8 +508,105 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui if (sourcePtr != null) this.utf8StringFromSequenceHook.Original(thisPtr, sourcePtr, sourceLen); else - thisPtr->Ctor(); // this is in clientstructs but you could do it manually too + thisPtr->Ctor(); // this is in ClientStructs but you could do it manually too return thisPtr; // this function shouldn't need to return but the original asm moves this into rax before returning so be safe? } } + +/// +/// Plugin-scoped version of a AddonLifecycle service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui +{ + [ServiceManager.ServiceDependency] + private readonly GameGui gameGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal GameGuiPluginScoped() + { + this.gameGuiService.UiHideToggled += this.UiHideToggledForward; + this.gameGuiService.HoveredItemChanged += this.HoveredItemForward; + this.gameGuiService.HoveredActionChanged += this.HoveredActionForward; + } + + /// + public event EventHandler? UiHideToggled; + + /// + public event EventHandler? HoveredItemChanged; + + /// + public event EventHandler? HoveredActionChanged; + + /// + public bool GameUiHidden => this.gameGuiService.GameUiHidden; + + /// + public ulong HoveredItem + { + get => this.gameGuiService.HoveredItem; + set => this.gameGuiService.HoveredItem = value; + } + + /// + public HoveredAction HoveredAction => this.gameGuiService.HoveredAction; + + /// + public void Dispose() + { + this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; + this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; + this.gameGuiService.HoveredActionChanged -= this.HoveredActionForward; + } + + /// + public bool OpenMapWithMapLink(MapLinkPayload mapLink) + => this.gameGuiService.OpenMapWithMapLink(mapLink); + + /// + public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos) + => this.gameGuiService.WorldToScreen(worldPos, out screenPos); + + /// + public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos, out bool inView) + => this.gameGuiService.WorldToScreen(worldPos, out screenPos, out inView); + + /// + public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000) + => this.gameGuiService.ScreenToWorld(screenPos, out worldPos, rayDistance); + + /// + public IntPtr GetUIModule() + => this.gameGuiService.GetUIModule(); + + /// + public IntPtr GetAddonByName(string name, int index = 1) + => this.gameGuiService.GetAddonByName(name, index); + + /// + public IntPtr FindAgentInterface(string addonName) + => this.gameGuiService.FindAgentInterface(addonName); + + /// + public unsafe IntPtr FindAgentInterface(void* addon) + => this.gameGuiService.FindAgentInterface(addon); + + /// + public IntPtr FindAgentInterface(IntPtr addonPtr) + => this.gameGuiService.FindAgentInterface(addonPtr); + + private void UiHideToggledForward(object sender, bool toggled) => this.UiHideToggled?.Invoke(sender, toggled); + + private void HoveredItemForward(object sender, ulong itemId) => this.HoveredItemChanged?.Invoke(sender, itemId); + + private void HoveredActionForward(object sender, HoveredAction hoverAction) => this.HoveredActionChanged?.Invoke(sender, hoverAction); +} From d96175ed16920f36e5851df736016657f11a9cda Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:10:36 -0700 Subject: [PATCH 084/122] Add DutyStatePluginScoped (#1382) --- Dalamud/Game/DutyState/DutyState.cs | 95 ++++++++++++++----- .../DutyState/DutyStateAddressResolver.cs | 2 - Dalamud/Plugin/Services/IDutyState.cs | 4 +- 3 files changed, 74 insertions(+), 27 deletions(-) diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 49fc874e3..2f117a492 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -1,25 +1,19 @@ -using System; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; -using Dalamud.Utility; namespace Dalamud.Game.DutyState; /// /// This class represents the state of the currently occupied duty. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public unsafe class DutyState : IDisposable, IServiceType, IDutyState +internal unsafe class DutyState : IDisposable, IServiceType, IDutyState { private readonly DutyStateAddressResolver address; private readonly Hook contentDirectorNetworkMessageHook; @@ -49,16 +43,16 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState private delegate byte SetupContentDirectNetworkMessageDelegate(IntPtr a1, IntPtr a2, ushort* a3); /// - public event EventHandler DutyStarted; + public event EventHandler? DutyStarted; /// - public event EventHandler DutyWiped; + public event EventHandler? DutyWiped; /// - public event EventHandler DutyRecommenced; + public event EventHandler? DutyRecommenced; /// - public event EventHandler DutyCompleted; + public event EventHandler? DutyCompleted; /// public bool IsDutyStarted { get; private set; } @@ -66,7 +60,7 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState private bool CompletedThisTerritory { get; set; } /// - void IDisposable.Dispose() + public void Dispose() { this.contentDirectorNetworkMessageHook.Dispose(); this.framework.Update -= this.FrameworkOnUpdateEvent; @@ -92,33 +86,33 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState // Duty Commenced case 0x4000_0001: this.IsDutyStarted = true; - this.DutyStarted.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyStarted?.Invoke(this, this.clientState.TerritoryType); break; // Party Wipe case 0x4000_0005: this.IsDutyStarted = false; - this.DutyWiped.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyWiped?.Invoke(this, this.clientState.TerritoryType); break; // Duty Recommence case 0x4000_0006: this.IsDutyStarted = true; - this.DutyRecommenced.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyRecommenced?.Invoke(this, this.clientState.TerritoryType); break; // Duty Completed Flytext Shown case 0x4000_0002 when !this.CompletedThisTerritory: this.IsDutyStarted = false; this.CompletedThisTerritory = true; - this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType); break; // Duty Completed case 0x4000_0003 when !this.CompletedThisTerritory: this.IsDutyStarted = false; this.CompletedThisTerritory = true; - this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType); break; } } @@ -161,11 +155,68 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState } private bool IsBoundByDuty() + => this.condition.Any(ConditionFlag.BoundByDuty, + ConditionFlag.BoundByDuty56, + ConditionFlag.BoundByDuty95); + + private bool IsInCombat() + => this.condition.Any(ConditionFlag.InCombat); +} + +/// +/// Plugin scoped version of DutyState. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState +{ + [ServiceManager.ServiceDependency] + private readonly DutyState dutyStateService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal DutyStatePluginScoped() { - return this.condition[ConditionFlag.BoundByDuty] || - this.condition[ConditionFlag.BoundByDuty56] || - this.condition[ConditionFlag.BoundByDuty95]; + this.dutyStateService.DutyStarted += this.DutyStartedForward; + this.dutyStateService.DutyWiped += this.DutyWipedForward; + this.dutyStateService.DutyRecommenced += this.DutyRecommencedForward; + this.dutyStateService.DutyCompleted += this.DutyCompletedForward; } - private bool IsInCombat() => this.condition[ConditionFlag.InCombat]; + /// + public event EventHandler? DutyStarted; + + /// + public event EventHandler? DutyWiped; + + /// + public event EventHandler? DutyRecommenced; + + /// + public event EventHandler? DutyCompleted; + + /// + public bool IsDutyStarted => this.dutyStateService.IsDutyStarted; + + /// + public void Dispose() + { + this.dutyStateService.DutyStarted -= this.DutyStartedForward; + this.dutyStateService.DutyWiped -= this.DutyWipedForward; + this.dutyStateService.DutyRecommenced -= this.DutyRecommencedForward; + this.dutyStateService.DutyCompleted -= this.DutyCompletedForward; + } + + private void DutyStartedForward(object sender, ushort territoryId) => this.DutyStarted?.Invoke(sender, territoryId); + + private void DutyWipedForward(object sender, ushort territoryId) => this.DutyWiped?.Invoke(sender, territoryId); + + private void DutyRecommencedForward(object sender, ushort territoryId) => this.DutyRecommenced?.Invoke(sender, territoryId); + + private void DutyCompletedForward(object sender, ushort territoryId) => this.DutyCompleted?.Invoke(sender, territoryId); } diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs index 801e5ef55..436883dc2 100644 --- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs +++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs @@ -1,5 +1,3 @@ -using System; - namespace Dalamud.Game.DutyState; /// diff --git a/Dalamud/Plugin/Services/IDutyState.cs b/Dalamud/Plugin/Services/IDutyState.cs index a2331364c..3d49f68cb 100644 --- a/Dalamud/Plugin/Services/IDutyState.cs +++ b/Dalamud/Plugin/Services/IDutyState.cs @@ -1,6 +1,4 @@ -using System; - -namespace Dalamud.Plugin.Services; +namespace Dalamud.Plugin.Services; /// /// This class represents the state of the currently occupied duty. From e3d688141c21daaa3d8b8f04bb7140edf83b956c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:20:05 -0700 Subject: [PATCH 085/122] Add ConditionPluginScoped (#1385) --- .../Game/ClientState/Conditions/Condition.cs | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index b72c91c74..585b762bf 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -10,13 +10,9 @@ namespace Dalamud.Game.ClientState.Conditions; /// /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public sealed partial class Condition : IServiceType, ICondition +internal sealed partial class Condition : IServiceType, ICondition { /// /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. @@ -122,7 +118,7 @@ public sealed partial class Condition : IServiceType, ICondition /// /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. /// -public sealed partial class Condition : IDisposable +internal sealed partial class Condition : IDisposable { private bool isDisposed; @@ -156,3 +152,52 @@ public sealed partial class Condition : IDisposable this.isDisposed = true; } } + +/// +/// Plugin-scoped version of a Condition service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition +{ + [ServiceManager.ServiceDependency] + private readonly Condition conditionService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal ConditionPluginScoped() + { + this.conditionService.ConditionChange += this.ConditionChangedForward; + } + + /// + public event ICondition.ConditionChangeDelegate? ConditionChange; + + /// + public int MaxEntries => this.conditionService.MaxEntries; + + /// + public IntPtr Address => this.conditionService.Address; + + /// + public bool this[int flag] => this.conditionService[flag]; + + /// + public void Dispose() + { + this.conditionService.ConditionChange -= this.ConditionChangedForward; + } + + /// + public bool Any() => this.conditionService.Any(); + + /// + public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags); + + private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value); +} From 3e613cffd0b032f68d8bcca4564731bc29eea691 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 12 Sep 2023 07:20:20 +0200 Subject: [PATCH 086/122] Update ClientStructs (#1371) (#1387) 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 ada62e7ae..7279a8f3c 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit ada62e7ae60de220d1f950b03ddb8d66e9e10daf +Subproject commit 7279a8f3ca6b79490184b05532af509781a89415 From 5ad464f2a83ff2126d906154b8cf6861efad74ae Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:53:00 +0200 Subject: [PATCH 087/122] Update ClientStructs (#1388) 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 7279a8f3c..a593cb163 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7279a8f3ca6b79490184b05532af509781a89415 +Subproject commit a593cb163e1c5d33b27d34df4d1ccc57d1e67643 From fd518f8e3f6123835733e6714f5840eae108bacf Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 20 Aug 2023 16:14:42 -0700 Subject: [PATCH 088/122] fix: Don't check for Wine Registry anymore - Renames `IsLinux` to `IsWine` to better reflect that this will return true for macOS as well. - Fixes a bug caused by misbehaving apps wanting to be helpful to Linux users - Also makes Wine checking far more resilient in cases where XL_WINEONLINUX isn't set. --- Dalamud/Utility/Util.cs | 42 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 5f2e4d5bf..63fc9faee 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -8,6 +8,7 @@ using System.Net.Http; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using Dalamud.Configuration.Internal; @@ -22,7 +23,6 @@ using Dalamud.Logging.Internal; using Dalamud.Networking.Http; using ImGuiNET; using Lumina.Excel.GeneratedSheets; -using Microsoft.Win32; using Serilog; namespace Dalamud.Utility; @@ -491,32 +491,30 @@ public static class Util } /// - /// Heuristically determine if Dalamud is running on Linux/WINE. + /// Determine if Dalamud is currently running within a Wine context (e.g. either on macOS or Linux). This method + /// will not return information about the host operating system. /// - /// Whether or not Dalamud is running on Linux/WINE. - public static bool IsLinux() + /// Returns true if Wine is detected, false otherwise. + public static bool IsWine() { - bool Check1() - { - return EnvironmentConfiguration.XlWineOnLinux; - } + if (EnvironmentConfiguration.XlWineOnLinux) return true; - bool Check2() - { - var hModule = NativeFunctions.GetModuleHandleW("ntdll.dll"); - var proc1 = NativeFunctions.GetProcAddress(hModule, "wine_get_version"); - var proc2 = NativeFunctions.GetProcAddress(hModule, "wine_get_build_id"); + var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); - return proc1 != IntPtr.Zero || proc2 != IntPtr.Zero; - } + // Test to see if any Wine specific exports exist. If they do, then we are running on Wine. + // The exports "wine_get_version", "wine_get_build_id", and "wine_get_host_version" will tend to be hidden + // by most Linux users (else FFXIV will want a macOS license), so we will additionally check some lesser-known + // exports as well. + return AnyProcExists( + ntdll, + "wine_get_version", + "wine_get_build_id", + "wine_get_host_version", + "wine_server_call", + "wine_unix_to_nt_file_name"); - bool Check3() - { - return Registry.CurrentUser.OpenSubKey(@"Software\Wine") != null || - Registry.LocalMachine.OpenSubKey(@"Software\Wine") != null; - } - - return Check1() || Check2() || Check3(); + bool AnyProcExists(nint handle, params string[] procs) => + procs.Any(p => NativeFunctions.GetProcAddress(handle, p) != nint.Zero); } /// From 2acba2b81f93cdd6dd766c9140389c9ec12ded08 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 12 Sep 2023 17:06:22 -0700 Subject: [PATCH 089/122] Update Dalamud.Boot to use extra import checks - Ports extra import checks to Dalamud Boot - Renames is_running_on_linux to is_running_on_wine - Update relevant logging string for VEH --- Dalamud.Boot/dllmain.cpp | 4 ++-- Dalamud.Boot/utils.cpp | 6 +++++- Dalamud.Boot/utils.h | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 9a741a47f..94f1c7d0f 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -133,8 +133,8 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { // ============================== VEH ======================================== // logging::I("Initializing VEH..."); - if (utils::is_running_on_linux()) { - logging::I("=> VEH was disabled, running on linux"); + if (utils::is_running_on_wine()) { + logging::I("=> VEH was disabled, running on wine"); } else if (g_startInfo.BootVehEnabled) { if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory)) logging::I("=> Done!"); diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index 79205eb8d..b45795045 100644 --- a/Dalamud.Boot/utils.cpp +++ b/Dalamud.Boot/utils.cpp @@ -578,7 +578,7 @@ std::vector utils::get_env_list(const wchar_t* pcszName) { return res; } -bool utils::is_running_on_linux() { +bool utils::is_running_on_wine() { if (get_env(L"XL_WINEONLINUX")) return true; HMODULE hntdll = GetModuleHandleW(L"ntdll.dll"); @@ -588,6 +588,10 @@ bool utils::is_running_on_linux() { return true; if (GetProcAddress(hntdll, "wine_get_host_version")) return true; + if (GetProcAddress(hntdll, "wine_server_call")) + return true; + if (GetProcAddress(hntdll, "wine_unix_to_nt_file_name")) + return true; return false; } diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h index 5d5c90dde..5e3caa4d6 100644 --- a/Dalamud.Boot/utils.h +++ b/Dalamud.Boot/utils.h @@ -264,7 +264,7 @@ namespace utils { return get_env_list(unicode::convert(pcszName).c_str()); } - bool is_running_on_linux(); + bool is_running_on_wine(); std::filesystem::path get_module_path(HMODULE hModule); From 0fca2c80c30457f9634e1d12be437ae51dc02b49 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 12 Sep 2023 18:14:54 -0700 Subject: [PATCH 090/122] Fix a build error, oops --- Dalamud/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 1975505a8..7ad794e42 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -166,7 +166,7 @@ public sealed class EntryPoint // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; - if (!Util.IsLinux()) + if (!Util.IsWine()) InitSymbolHandler(info); var dalamud = new Dalamud(info, configuration, mainThreadContinueEvent); From a7aacb15e4603a367e2f980578271a9a639d8852 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 12 Sep 2023 18:22:20 -0700 Subject: [PATCH 091/122] Add XL_PLATFORM env var, Util.GetHostPlatform() - New env var XL_PLATFORM allows launcher to inform Dalamud of the current platform (one of Windows, macOS, or Linux). - New method Util.GetHostPlatform() provides a best guess for the current platform. This method will respect XL_PLATFORM if set, but will otherwise resort to heuristic checks. --- Dalamud/Utility/Util.cs | 68 +++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 63fc9faee..5be4e4fc4 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -1,10 +1,8 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; -using System.Net.Http; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; @@ -16,11 +14,10 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; -using Dalamud.Networking.Http; +using Dalamud.Memory; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Serilog; @@ -38,7 +35,7 @@ public static class Util private static ulong moduleStartAddr; private static ulong moduleEndAddr; - + /// /// Gets the assembly version of Dalamud. /// @@ -498,6 +495,7 @@ public static class Util public static bool IsWine() { if (EnvironmentConfiguration.XlWineOnLinux) return true; + if (Environment.GetEnvironmentVariable("XL_PLATFORM") is not null and not "Windows") return true; var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); @@ -506,17 +504,46 @@ public static class Util // by most Linux users (else FFXIV will want a macOS license), so we will additionally check some lesser-known // exports as well. return AnyProcExists( - ntdll, - "wine_get_version", - "wine_get_build_id", + ntdll, + "wine_get_version", + "wine_get_build_id", "wine_get_host_version", - "wine_server_call", + "wine_server_call", "wine_unix_to_nt_file_name"); bool AnyProcExists(nint handle, params string[] procs) => procs.Any(p => NativeFunctions.GetProcAddress(handle, p) != nint.Zero); } + /// + /// Gets the best guess for the current host's platform based on the XL_PLATFORM environment variable or + /// heuristics. + /// + /// + /// This method is not perfectly reliable if XL_PLATFORM is unset. For example, macOS users running with + /// exports hidden will be marked as Linux users. Better heuristic checks for macOS are needed in order to fix this. + /// + /// Returns the that Dalamud is currently running on. + public static OSPlatform GetHostPlatform() + { + switch (Environment.GetEnvironmentVariable("XL_PLATFORM")) + { + case "Windows": return OSPlatform.Windows; + case "MacOS": return OSPlatform.OSX; + case "Linux": return OSPlatform.Linux; + } + + if (IsWine()) + { + GetWineHostVersion(out var platform, out _); + if (platform == "Darwin") return OSPlatform.OSX; // only happens on macOS without export hides (mac license) + + return OSPlatform.Linux; + } + + return OSPlatform.Windows; + } + /// /// Heuristically determine if the Windows version is higher than Windows 11's first build. /// @@ -683,7 +710,7 @@ public static class Util ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X")); } } - + private static unsafe void ShowValue(ulong addr, IEnumerable path, Type type, object value) { if (type.IsPointer) @@ -738,4 +765,25 @@ public static class Util } } } + + private static unsafe bool GetWineHostVersion(out string? platform, out string? version) + { + platform = null; + version = null; + + var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); + var methodPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version"); + + if (methodPtr == nint.Zero) return false; + + var methodDelegate = (delegate* unmanaged[Fastcall])methodPtr; + methodDelegate(out var platformPtr, out var versionPtr); + + if (platformPtr == null) return false; + + platform = MemoryHelper.ReadStringNullTerminated((nint)platformPtr); + if (versionPtr != null) version = MemoryHelper.ReadStringNullTerminated((nint)versionPtr); + + return true; + } } From 8911d4ebc2e2134661360de3c2e0e26b6e202c0c Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 12 Sep 2023 18:33:41 -0700 Subject: [PATCH 092/122] Re-add SourceContext property check Technically an API breakage, some plugins are passing this for logging. Slated for removal in API 9, however. --- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 3303a2280..0febc0fc4 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -504,14 +504,16 @@ internal class ConsoleWindow : Window, IDisposable HasException = logEvent.Exception != null, }; + // TODO (v9): Remove SourceContext property check. if (logEvent.Properties.ContainsKey("Dalamud.ModuleName")) { entry.Source = "DalamudInternal"; } - else if (logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) && - sourceProp is ScalarValue { Value: string value }) + else if ((logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) || + logEvent.Properties.TryGetValue("SourceContext", out sourceProp)) && + sourceProp is ScalarValue { Value: string sourceValue }) { - entry.Source = value; + entry.Source = sourceValue; } this.logText.Add(entry); From 088cf8c3922022d5fb576f05b876d7263032226f Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 13 Sep 2023 00:23:17 -0700 Subject: [PATCH 093/122] Remove fancy Darwin checking code --- Dalamud/Utility/Util.cs | 38 +++++++------------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 5be4e4fc4..78edda3dd 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -520,8 +520,8 @@ public static class Util /// heuristics. /// /// - /// This method is not perfectly reliable if XL_PLATFORM is unset. For example, macOS users running with - /// exports hidden will be marked as Linux users. Better heuristic checks for macOS are needed in order to fix this. + /// macOS users running without XL_PLATFORM being set will be reported as Linux users. Due to the way our + /// Wines work, there isn't a great (consistent) way to split the two apart if we're not told. /// /// Returns the that Dalamud is currently running on. public static OSPlatform GetHostPlatform() @@ -532,16 +532,13 @@ public static class Util case "MacOS": return OSPlatform.OSX; case "Linux": return OSPlatform.Linux; } - - if (IsWine()) - { - GetWineHostVersion(out var platform, out _); - if (platform == "Darwin") return OSPlatform.OSX; // only happens on macOS without export hides (mac license) - return OSPlatform.Linux; - } + // n.b. we had some fancy code here to check if the Wine host version returned "Darwin" but apparently + // *all* our Wines report Darwin if exports aren't hidden. As such, it is effectively impossible (without some + // (very cursed and inaccurate heuristics) to determine if we're on macOS or Linux unless we're explicitly told + // by our launcher. See commit a7aacb15e4603a367e2f980578271a9a639d8852 for the old check. - return OSPlatform.Windows; + return IsWine() ? OSPlatform.Linux : OSPlatform.Windows; } /// @@ -765,25 +762,4 @@ public static class Util } } } - - private static unsafe bool GetWineHostVersion(out string? platform, out string? version) - { - platform = null; - version = null; - - var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); - var methodPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version"); - - if (methodPtr == nint.Zero) return false; - - var methodDelegate = (delegate* unmanaged[Fastcall])methodPtr; - methodDelegate(out var platformPtr, out var versionPtr); - - if (platformPtr == null) return false; - - platform = MemoryHelper.ReadStringNullTerminated((nint)platformPtr); - if (versionPtr != null) version = MemoryHelper.ReadStringNullTerminated((nint)versionPtr); - - return true; - } } From 6eba1415f546e38d5fab31d8f71a71dc379bf254 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 22:18:41 -0700 Subject: [PATCH 094/122] Add IChatGui --- Dalamud/Game/Gui/ChatGui.cs | 112 ++++++-------------------- Dalamud/Plugin/Services/IChatGui.cs | 119 ++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 89 deletions(-) create mode 100644 Dalamud/Plugin/Services/IChatGui.cs diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 93185caf9..d8154b41d 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -22,7 +23,10 @@ namespace Dalamud.Game.Gui; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class ChatGui : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed class ChatGui : IDisposable, IServiceType, IChatGui { private readonly ChatGuiAddressResolver address; @@ -51,45 +55,7 @@ public sealed class ChatGui : IDisposable, IServiceType this.populateItemLinkHook = Hook.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); this.interactableLinkClickedHook = Hook.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); } - - /// - /// A delegate type used with the event. - /// - /// The type of chat. - /// The sender ID. - /// The sender name. - /// The message sent. - /// A value indicating whether the message was handled or should be propagated. - public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); - - /// - /// A delegate type used with the event. - /// - /// The type of chat. - /// The sender ID. - /// The sender name. - /// The message sent. - /// A value indicating whether the message was handled or should be propagated. - public delegate void OnCheckMessageHandledDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); - - /// - /// A delegate type used with the event. - /// - /// The type of chat. - /// The sender ID. - /// The sender name. - /// The message sent. - public delegate void OnMessageHandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message); - - /// - /// A delegate type used with the event. - /// - /// The type of chat. - /// The sender ID. - /// The sender name. - /// The message sent. - public delegate void OnMessageUnhandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message); - + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr PrintMessageDelegate(IntPtr manager, XivChatType chatType, IntPtr senderName, IntPtr message, uint senderId, IntPtr parameter); @@ -99,34 +65,22 @@ public sealed class ChatGui : IDisposable, IServiceType [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr); - /// - /// Event that will be fired when a chat message is sent to chat by the game. - /// - public event OnMessageDelegate ChatMessage; + /// + public event IChatGui.OnMessageDelegate ChatMessage; - /// - /// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true. - /// - public event OnCheckMessageHandledDelegate CheckMessageHandled; + /// + public event IChatGui.OnCheckMessageHandledDelegate CheckMessageHandled; - /// - /// Event that will be fired when a chat message is handled by Dalamud or a Plugin. - /// - public event OnMessageHandledDelegate ChatMessageHandled; + /// + public event IChatGui.OnMessageHandledDelegate ChatMessageHandled; - /// - /// Event that will be fired when a chat message is not handled by Dalamud or a Plugin. - /// - public event OnMessageUnhandledDelegate ChatMessageUnhandled; + /// + public event IChatGui.OnMessageUnhandledDelegate ChatMessageUnhandled; - /// - /// Gets the ID of the last linked item. - /// + /// public int LastLinkedItemId { get; private set; } - /// - /// Gets the flags of the last linked item. - /// + /// public byte LastLinkedItemFlags { get; private set; } /// @@ -139,21 +93,13 @@ public sealed class ChatGui : IDisposable, IServiceType this.interactableLinkClickedHook.Dispose(); } - /// - /// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void PrintChat(XivChatEntry chat) { this.chatQueue.Enqueue(chat); } - /// - /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void Print(string message) { // Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message); @@ -164,11 +110,7 @@ public sealed class ChatGui : IDisposable, IServiceType }); } - /// - /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void Print(SeString message) { // Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue); @@ -179,11 +121,7 @@ public sealed class ChatGui : IDisposable, IServiceType }); } - /// - /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to - /// the queue, later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void PrintError(string message) { // Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message); @@ -194,11 +132,7 @@ public sealed class ChatGui : IDisposable, IServiceType }); } - /// - /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to - /// the queue, later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void PrintError(SeString message) { // Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue); @@ -330,7 +264,7 @@ public sealed class ChatGui : IDisposable, IServiceType { try { - var messageHandledDelegate = @delegate as OnCheckMessageHandledDelegate; + var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate; messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) @@ -346,7 +280,7 @@ public sealed class ChatGui : IDisposable, IServiceType { try { - var messageHandledDelegate = @delegate as OnMessageDelegate; + var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate; messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs new file mode 100644 index 000000000..1d71bb886 --- /dev/null +++ b/Dalamud/Plugin/Services/IChatGui.cs @@ -0,0 +1,119 @@ +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Plugin.Services; + +/// +/// This class handles interacting with the native chat UI. +/// +public interface IChatGui +{ + /// + /// A delegate type used with the event. + /// + /// The type of chat. + /// The sender ID. + /// The sender name. + /// The message sent. + /// A value indicating whether the message was handled or should be propagated. + public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); + + /// + /// A delegate type used with the event. + /// + /// The type of chat. + /// The sender ID. + /// The sender name. + /// The message sent. + /// A value indicating whether the message was handled or should be propagated. + public delegate void OnCheckMessageHandledDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); + + /// + /// A delegate type used with the event. + /// + /// The type of chat. + /// The sender ID. + /// The sender name. + /// The message sent. + public delegate void OnMessageHandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message); + + /// + /// A delegate type used with the event. + /// + /// The type of chat. + /// The sender ID. + /// The sender name. + /// The message sent. + public delegate void OnMessageUnhandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message); + + /// + /// Event that will be fired when a chat message is sent to chat by the game. + /// + public event OnMessageDelegate ChatMessage; + + /// + /// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true. + /// + public event OnCheckMessageHandledDelegate CheckMessageHandled; + + /// + /// Event that will be fired when a chat message is handled by Dalamud or a Plugin. + /// + public event OnMessageHandledDelegate ChatMessageHandled; + + /// + /// Event that will be fired when a chat message is not handled by Dalamud or a Plugin. + /// + public event OnMessageUnhandledDelegate ChatMessageUnhandled; + + /// + /// Gets the ID of the last linked item. + /// + public int LastLinkedItemId { get; } + + /// + /// Gets the flags of the last linked item. + /// + public byte LastLinkedItemFlags { get; } + + /// + /// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue, + /// later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void PrintChat(XivChatEntry chat); + + /// + /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, + /// later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void Print(string message); + + /// + /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, + /// later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void Print(SeString message); + + /// + /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to + /// the queue, later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void PrintError(string message); + + /// + /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to + /// the queue, later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void PrintError(SeString message); + + /// + /// Process a chat queue. + /// + public void UpdateQueue(); +} From 7080087e9db6469b7ff824fab28d6e7cca6822a4 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 13 Sep 2023 19:06:28 -0700 Subject: [PATCH 095/122] Scope ChatGui --- Dalamud/Game/ChatHandlers.cs | 4 +- Dalamud/Game/Gui/ChatGui.cs | 224 +++++++++++++++++------ Dalamud/Plugin/Internal/PluginManager.cs | 4 +- Dalamud/Plugin/Services/IChatGui.cs | 38 ++-- 4 files changed, 186 insertions(+), 84 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index ed69b7bbe..1d82e5f9c 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -264,7 +264,7 @@ public class ChatHandlers : IServiceType if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion)) { - chatGui.PrintChat(new XivChatEntry + chatGui.Print(new XivChatEntry { Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."), Type = XivChatType.Notice, @@ -321,7 +321,7 @@ public class ChatHandlers : IServiceType } else { - chatGui.PrintChat(new XivChatEntry + chatGui.Print(new XivChatEntry { Message = new SeString(new List() { diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index d8154b41d..2fbeb404e 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -20,13 +20,9 @@ namespace Dalamud.Game.Gui; /// /// This class handles interacting with the native chat UI. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public sealed class ChatGui : IDisposable, IServiceType, IChatGui +internal sealed class ChatGui : IDisposable, IServiceType, IChatGui { private readonly ChatGuiAddressResolver address; @@ -66,16 +62,16 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr); /// - public event IChatGui.OnMessageDelegate ChatMessage; + public event IChatGui.OnMessageDelegate? ChatMessage; /// - public event IChatGui.OnCheckMessageHandledDelegate CheckMessageHandled; + public event IChatGui.OnCheckMessageHandledDelegate? CheckMessageHandled; /// - public event IChatGui.OnMessageHandledDelegate ChatMessageHandled; + public event IChatGui.OnMessageHandledDelegate? ChatMessageHandled; /// - public event IChatGui.OnMessageUnhandledDelegate ChatMessageUnhandled; + public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; /// public int LastLinkedItemId { get; private set; } @@ -94,55 +90,35 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui } /// - public void PrintChat(XivChatEntry chat) + public void Print(XivChatEntry chat) { this.chatQueue.Enqueue(chat); } - + /// - public void Print(string message) + public void Print(string message, string? messageTag = null, ushort? tagColor = null) { - // Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message); - this.PrintChat(new XivChatEntry - { - Message = message, - Type = this.configuration.GeneralChatType, - }); + this.PrintTagged(message, this.configuration.GeneralChatType, messageTag, tagColor); } - + /// - public void Print(SeString message) + public void Print(SeString message, string? messageTag = null, ushort? tagColor = null) { - // Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue); - this.PrintChat(new XivChatEntry - { - Message = message, - Type = this.configuration.GeneralChatType, - }); + this.PrintTagged(message, this.configuration.GeneralChatType, messageTag, tagColor); } - + /// - public void PrintError(string message) + public void PrintError(string message, string? messageTag = null, ushort? tagColor = null) { - // Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message); - this.PrintChat(new XivChatEntry - { - Message = message, - Type = XivChatType.Urgent, - }); + this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor); } - + /// - public void PrintError(SeString message) + public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null) { - // Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue); - this.PrintChat(new XivChatEntry - { - Message = message, - Type = XivChatType.Urgent, - }); + this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor); } - + /// /// Process a chat queue. /// @@ -176,7 +152,7 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui /// A payload for handling. internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action commandAction) { - var payload = new DalamudLinkPayload() { Plugin = pluginName, CommandId = commandId }; + var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId }; this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction); return payload; } @@ -200,20 +176,63 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui /// The ID of the command to be removed. internal void RemoveChatLinkHandler(string pluginName, uint commandId) { - if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId))) - { - this.dalamudLinkHandlers.Remove((pluginName, commandId)); - } + this.dalamudLinkHandlers.Remove((pluginName, commandId)); } [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(GameGui gameGui, LibcFunction libcFunction) + private void ContinueConstruction() { this.printMessageHook.Enable(); this.populateItemLinkHook.Enable(); this.interactableLinkClickedHook.Enable(); } + private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color) + { + var builder = new SeStringBuilder(); + + if (!tag.IsNullOrEmpty()) + { + if (color is not null) + { + builder.AddUiForeground($"[{tag}] ", color.Value); + } + else + { + builder.AddText($"[{tag}] "); + } + } + + this.Print(new XivChatEntry + { + Message = builder.AddText(message).Build(), + Type = channel, + }); + } + + private void PrintTagged(SeString message, XivChatType channel, string? tag, ushort? color) + { + var builder = new SeStringBuilder(); + + if (!tag.IsNullOrEmpty()) + { + if (color is not null) + { + builder.AddUiForeground($"[{tag}] ", color.Value); + } + else + { + builder.AddText($"[{tag}] "); + } + } + + this.Print(new XivChatEntry + { + Message = builder.Build().Append(message), + Type = channel, + }); + } + private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr) { try @@ -232,7 +251,7 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui } } - private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chattype, IntPtr pSenderName, IntPtr pMessage, uint senderid, IntPtr parameter) + private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chatType, IntPtr pSenderName, IntPtr pMessage, uint senderId, IntPtr parameter) { var retVal = IntPtr.Zero; @@ -259,13 +278,13 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui // Call events var isHandled = false; - var invocationList = this.CheckMessageHandled.GetInvocationList(); + var invocationList = this.CheckMessageHandled!.GetInvocationList(); foreach (var @delegate in invocationList) { try { var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate; - messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); + messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) { @@ -275,13 +294,13 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui if (!isHandled) { - invocationList = this.ChatMessage.GetInvocationList(); + invocationList = this.ChatMessage!.GetInvocationList(); foreach (var @delegate in invocationList) { try { var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate; - messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); + messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) { @@ -324,12 +343,12 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui // Print the original chat if it's handled. if (isHandled) { - this.ChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage); + this.ChatMessageHandled?.Invoke(chatType, senderId, parsedSender, parsedMessage); } else { - retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter); - this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage); + retVal = this.printMessageHook.Original(manager, chatType, senderPtr, messagePtr, senderId, parameter); + this.ChatMessageUnhandled?.Invoke(chatType, senderId, parsedSender, parsedMessage); } if (this.baseAddress == IntPtr.Zero) @@ -341,7 +360,7 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui catch (Exception ex) { Log.Error(ex, "Exception on OnChatMessage hook."); - retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter); + retVal = this.printMessageHook.Original(manager, chatType, pSenderName, pMessage, senderId, parameter); } return retVal; @@ -373,10 +392,10 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui var linkPayload = payloads[0]; if (linkPayload is DalamudLinkPayload link) { - if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId))) + if (this.dalamudLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value)) { Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}"); - this.dalamudLinkHandlers[(link.Plugin, link.CommandId)].Invoke(link.CommandId, new SeString(payloads)); + value.Invoke(link.CommandId, new SeString(payloads)); } else { @@ -390,3 +409,88 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui } } } + +/// +/// Plugin scoped version of ChatGui. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui +{ + [ServiceManager.ServiceDependency] + private readonly ChatGui chatGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal ChatGuiPluginScoped() + { + this.chatGuiService.ChatMessage += this.OnMessageForward; + this.chatGuiService.CheckMessageHandled += this.OnCheckMessageForward; + this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward; + this.chatGuiService.ChatMessageUnhandled += this.OnMessageUnhandledForward; + } + + /// + public event IChatGui.OnMessageDelegate? ChatMessage; + + /// + public event IChatGui.OnCheckMessageHandledDelegate? CheckMessageHandled; + + /// + public event IChatGui.OnMessageHandledDelegate? ChatMessageHandled; + + /// + public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; + + /// + public int LastLinkedItemId => this.chatGuiService.LastLinkedItemId; + + /// + public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags; + + /// + public void Dispose() + { + this.chatGuiService.ChatMessage -= this.OnMessageForward; + this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward; + this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward; + this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward; + } + + /// + public void Print(XivChatEntry chat) + => this.chatGuiService.Print(chat); + + /// + public void Print(string message, string? messageTag = null, ushort? tagColor = null) + => this.chatGuiService.Print(message, messageTag, tagColor); + + /// + public void Print(SeString message, string? messageTag = null, ushort? tagColor = null) + => this.chatGuiService.Print(message, messageTag, tagColor); + + /// + public void PrintError(string message, string? messageTag = null, ushort? tagColor = null) + => this.chatGuiService.PrintError(message, messageTag, tagColor); + + /// + public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null) + => this.chatGuiService.PrintError(message, messageTag, tagColor); + + private void OnMessageForward(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) + => this.ChatMessage?.Invoke(type, senderId, ref sender, ref message, ref isHandled); + + private void OnCheckMessageForward(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) + => this.CheckMessageHandled?.Invoke(type, senderId, ref sender, ref message, ref isHandled); + + private void OnMessageHandledForward(XivChatType type, uint senderId, SeString sender, SeString message) + => this.ChatMessageHandled?.Invoke(type, senderId, sender, message); + + private void OnMessageUnhandledForward(XivChatType type, uint senderId, SeString sender, SeString message) + => this.ChatMessageUnhandled?.Invoke(type, senderId, sender, message); +} diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index b160038d7..691d5f729 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -285,7 +285,7 @@ internal partial class PluginManager : IDisposable, IServiceType if (updateMetadata is { Count: > 0 }) { - chatGui.PrintChat(new XivChatEntry + chatGui.Print(new XivChatEntry { Message = new SeString(new List() { @@ -308,7 +308,7 @@ internal partial class PluginManager : IDisposable, IServiceType } else { - chatGui.PrintChat(new XivChatEntry + chatGui.Print(new XivChatEntry { Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version), Type = XivChatType.Urgent, diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs index 1d71bb886..bafdabbb5 100644 --- a/Dalamud/Plugin/Services/IChatGui.cs +++ b/Dalamud/Plugin/Services/IChatGui.cs @@ -78,42 +78,40 @@ public interface IChatGui public byte LastLinkedItemFlags { get; } /// - /// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void PrintChat(XivChatEntry chat); + public void Print(XivChatEntry chat); /// - /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void Print(string message); + /// String to prepend message with "[messageTag] ". + /// Color to display the message tag with. + public void Print(string message, string? messageTag = null, ushort? tagColor = null); /// - /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void Print(SeString message); + /// String to prepend message with "[messageTag] ". + /// Color to display the message tag with. + public void Print(SeString message, string? messageTag = null, ushort? tagColor = null); /// - /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to - /// the queue, later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void PrintError(string message); + /// String to prepend message with "[messageTag] ". + /// Color to display the message tag with. + public void PrintError(string message, string? messageTag = null, ushort? tagColor = null); /// - /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to - /// the queue, later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void PrintError(SeString message); - - /// - /// Process a chat queue. - /// - public void UpdateQueue(); + /// String to prepend message with "[messageTag] ". + /// Color to display the message tag with. + public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null); } From 947cd79bcae588ed30c0ea36b62b5e61ecace971 Mon Sep 17 00:00:00 2001 From: kal <35899782+kalilistic@users.noreply.github.com> Date: Sat, 16 Sep 2023 20:54:29 -0400 Subject: [PATCH 096/122] chore: remove remaining obsolete icons --- .../Interface/FontAwesome/FontAwesomeIcon.cs | 62 +++---------------- 1 file changed, 10 insertions(+), 52 deletions(-) diff --git a/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs b/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs index 3d21ea86c..f88d7f8f0 100644 --- a/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs +++ b/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // Generated by Dalamud.FASharpGen - don't modify this file directly. -// Font-Awesome Version: 6.3.0 +// Font-Awesome Version: 6.4.2 // //------------------------------------------------------------------------------ @@ -19,12 +19,6 @@ public enum FontAwesomeIcon /// None = 0, - /// - /// The Font Awesome "acquisitionsincorporated" icon unicode character. - /// - [Obsolete] - AcquisitionsIncorporated = 0xF6AF, - /// /// The Font Awesome "rectangle-ad" icon unicode character. /// @@ -43,7 +37,7 @@ public enum FontAwesomeIcon /// The Font Awesome "address-card" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "address card", "about", "contact", "id", "identification", "postcard", "profile", "registration" })] - [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Users + People" })] + [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Alphabet", "Business", "Communication", "Users + People" })] AddressCard = 0xF2BB, /// @@ -53,12 +47,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Design", "Editing", "Photos + Images", "Shapes" })] Adjust = 0xF042, - /// - /// The Font Awesome "adobe" icon unicode character. - /// - [Obsolete] - Adobe = 0xF778, - /// /// The Font Awesome "spray-can-sparkles" icon unicode character. /// @@ -884,7 +872,7 @@ public enum FontAwesomeIcon /// The Font Awesome "binoculars" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "binoculars", "glasses", "magnify", "scenic", "spyglass", "view" })] - [FontAwesomeCategoriesAttribute(new[] { "Camping", "Maps", "Nature" })] + [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Camping", "Maps", "Nature" })] Binoculars = 0xF1E5, /// @@ -1359,7 +1347,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bullseye" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bullseye", "archery", "goal", "objective", "target" })] + [FontAwesomeSearchTerms(new[] { "bullseye", "archery", "goal", "objective", "strategy", "target" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Marketing", "Toggle" })] Bullseye = 0xF140, @@ -2202,7 +2190,7 @@ public enum FontAwesomeIcon /// The Font Awesome "gear" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "cog", "cogwheel", "gear", "mechanical", "settings", "sprocket", "tool", "wheel" })] - [FontAwesomeCategoriesAttribute(new[] { "Spinners" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Editing", "Spinners" })] Cog = 0xF013, /// @@ -3423,14 +3411,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "flask" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "flask", "beaker", "experimental", "labs", "science" })] + [FontAwesomeSearchTerms(new[] { "flask", "beaker", "chemicals", "experiment", "experimental", "labs", "liquid", "potion", "science", "vial" })] [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage", "Maps", "Medical + Health", "Science" })] Flask = 0xF0C3, /// /// The Font Awesome "flask-vial" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "flask vial", "ampule", "chemistry", "lab", "laboratory", "test", "test tube" })] + [FontAwesomeSearchTerms(new[] { "flask vial", " beaker", " chemicals", " experiment", " experimental", " labs", " liquid", " science", " vial", "ampule", "chemistry", "lab", "laboratory", "potion", "test", "test tube" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Science" })] FlaskVial = 0xE4F3, @@ -5088,7 +5076,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "lightbulb" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "lightbulb", "bulb", "comic", "electric", "energy", "idea", "inspiration", "light", "light bulb" })] + [FontAwesomeSearchTerms(new[] { "lightbulb", " comic", " electric", " idea", " innovation", " inspiration", " light", " light bulb", " bulb", "bulb", "comic", "electric", "energy", "idea", "inspiration", "mechanical" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Household", "Maps", "Marketing" })] Lightbulb = 0xF0EB, @@ -5270,7 +5258,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "magnifying-glass-chart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass chart", "analysis", "chart" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass chart", " data", " graph", " intelligence", "analysis", "chart", "market" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Humanitarian", "Marketing" })] MagnifyingGlassChart = 0xE522, @@ -5484,12 +5472,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Weather" })] Meteor = 0xF753, - /// - /// The Font Awesome "microblog" icon unicode character. - /// - [Obsolete] - Microblog = 0xF91A, - /// /// The Font Awesome "microchip" icon unicode character. /// @@ -5676,7 +5658,7 @@ public enum FontAwesomeIcon /// The Font Awesome "monument" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "monument", "building", "historic", "landmark", "memorable" })] - [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Travel + Hotel" })] + [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Maps", "Travel + Hotel" })] Monument = 0xF5A6, /// @@ -6043,12 +6025,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing" })] PenNib = 0xF5AD, - /// - /// The Font Awesome "pennyarcade" icon unicode character. - /// - [Obsolete] - PennyArcade = 0xF704, - /// /// The Font Awesome "square-pen" icon unicode character. /// @@ -6415,12 +6391,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Files", "Film + Video", "Photos + Images", "Social" })] PhotoVideo = 0xF87C, - /// - /// The Font Awesome "piedpipersquare" icon unicode character. - /// - [Obsolete] - PiedPiperSquare = 0xF91E, - /// /// The Font Awesome "piggy-bank" icon unicode character. /// @@ -8720,12 +8690,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Travel + Hotel" })] TreeCity = 0xE587, - /// - /// The Font Awesome "tripadvisor" icon unicode character. - /// - [Obsolete] - Tripadvisor = 0xF262, - /// /// The Font Awesome "trophy" icon unicode character. /// @@ -8887,12 +8851,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] UndoAlt = 0xF2EA, - /// - /// The Font Awesome "unity" icon unicode character. - /// - [Obsolete] - Unity = 0xF949, - /// /// The Font Awesome "universal-access" icon unicode character. /// From 85bb5229d9c4b8bc84d1bc1fbd85b574ba59cc25 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 16 Sep 2023 18:00:08 -0700 Subject: [PATCH 097/122] Update ClientStructs - Fix build errors caused by obsoletes --- Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs | 4 +--- .../Game/ClientState/Objects/SubKinds/PlayerCharacter.cs | 4 +--- Dalamud/Game/ClientState/Objects/Types/Character.cs | 7 +++---- lib/FFXIVClientStructs | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs b/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs index 59f32e33d..add7a7f9f 100644 --- a/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs +++ b/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Game.ClientState.Objects.Enums; namespace Dalamud.Game.ClientState.Objects.Types; @@ -25,5 +23,5 @@ public unsafe class BattleNpc : BattleChara public BattleNpcSubKind BattleNpcKind => (BattleNpcSubKind)this.Struct->Character.GameObject.SubKind; /// - public override ulong TargetObjectId => this.Struct->Character.TargetObjectID; + public override ulong TargetObjectId => this.Struct->Character.TargetId; } diff --git a/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs b/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs index 7fc9c0079..9de11e3ec 100644 --- a/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs +++ b/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Resolvers; @@ -33,5 +31,5 @@ public unsafe class PlayerCharacter : BattleChara /// /// Gets the target actor ID of the PlayerCharacter. /// - public override ulong TargetObjectId => this.Struct->Character.PlayerTargetObjectID; + public override ulong TargetObjectId => this.Struct->Character.LookTargetId; } diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index ee8418362..a1eb52edc 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.Text.SeStringHandling; @@ -87,7 +85,7 @@ public unsafe class Character : GameObject /// /// Gets the target object ID of the character. /// - public override ulong TargetObjectId => this.Struct->TargetObjectID; + public override ulong TargetObjectId => this.Struct->TargetId; /// /// Gets the name ID of the character. @@ -115,5 +113,6 @@ public unsafe class Character : GameObject /// /// Gets the underlying structure. /// - protected internal new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address; + protected internal new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct => + (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address; } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 7279a8f3c..06e3ca233 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7279a8f3ca6b79490184b05532af509781a89415 +Subproject commit 06e3ca2336031ba86ef95d022a2af722e5d00a7e From a9a0980372c3cecc405daa952c5cb2da9420b064 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 16 Sep 2023 18:07:19 -0700 Subject: [PATCH 098/122] Fix random build warnings. --- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 5 ++- Dalamud/Interface/ColorHelpers.cs | 39 ++++++++++++------- Dalamud/Plugin/Services/IPluginLog.cs | 6 +-- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 85c6a4a39..e3ea74f2d 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -1,6 +1,4 @@ -using System; using System.Runtime.InteropServices; - using Dalamud.Game.Gui.PartyFinder.Internal; using Dalamud.Game.Gui.PartyFinder.Types; using Dalamud.Hooking; @@ -128,6 +126,9 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu } } +/// +/// A scoped variant of the PartyFinderGui service. +/// [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.ScopedService] diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs index 71f959292..ad9eedaa9 100644 --- a/Dalamud/Interface/ColorHelpers.cs +++ b/Dalamud/Interface/ColorHelpers.cs @@ -1,4 +1,4 @@ -using System; +using System.Diagnostics.CodeAnalysis; using System.Numerics; namespace Dalamud.Interface; @@ -8,6 +8,17 @@ namespace Dalamud.Interface; /// public static class ColorHelpers { + /// + /// A struct representing a color using HSVA coordinates. + /// + /// The hue represented by this struct. + /// The saturation represented by this struct. + /// The value represented by this struct. + /// The alpha represented by this struct. + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", + Justification = "I don't like it.")] + public record struct HsvaColor(float H, float S, float V, float A); + /// /// Pack a vector4 color into a uint for use in ImGui APIs. /// @@ -22,7 +33,7 @@ public static class ColorHelpers 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. /// @@ -37,7 +48,7 @@ public static class ColorHelpers return new Vector4(r, g, b, a); } - + /// /// Convert a RGBA color in the range of 0.f to 1.f to a HSV color. /// @@ -146,7 +157,7 @@ public static class ColorHelpers return new Vector4(r, g, b, hsv.A); } - + /// /// Lighten a color. /// @@ -159,7 +170,7 @@ public static class ColorHelpers hsv.V += amount; return HsvToRgb(hsv); } - + /// /// Lighten a color. /// @@ -168,7 +179,7 @@ public static class ColorHelpers /// The lightened color. public static uint Lighten(uint color, float amount) => RgbaVector4ToUint(Lighten(RgbaUintToVector4(color), amount)); - + /// /// Darken a color. /// @@ -181,7 +192,7 @@ public static class ColorHelpers hsv.V -= amount; return HsvToRgb(hsv); } - + /// /// Darken a color. /// @@ -190,7 +201,7 @@ public static class ColorHelpers /// The darkened color. public static uint Darken(uint color, float amount) => RgbaVector4ToUint(Darken(RgbaUintToVector4(color), amount)); - + /// /// Saturate a color. /// @@ -203,7 +214,7 @@ public static class ColorHelpers hsv.S += amount; return HsvToRgb(hsv); } - + /// /// Saturate a color. /// @@ -212,7 +223,7 @@ public static class ColorHelpers /// The saturated color. public static uint Saturate(uint color, float amount) => RgbaVector4ToUint(Saturate(RgbaUintToVector4(color), amount)); - + /// /// Desaturate a color. /// @@ -225,7 +236,7 @@ public static class ColorHelpers hsv.S -= amount; return HsvToRgb(hsv); } - + /// /// Desaturate a color. /// @@ -234,7 +245,7 @@ public static class ColorHelpers /// The desaturated color. public static uint Desaturate(uint color, float amount) => RgbaVector4ToUint(Desaturate(RgbaUintToVector4(color), amount)); - + /// /// Fade a color. /// @@ -247,7 +258,7 @@ public static class ColorHelpers hsv.A -= amount; return HsvToRgb(hsv); } - + /// /// Fade a color. /// @@ -256,6 +267,4 @@ public static class ColorHelpers /// 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); } diff --git a/Dalamud/Plugin/Services/IPluginLog.cs b/Dalamud/Plugin/Services/IPluginLog.cs index 87876f36f..62f9e8728 100644 --- a/Dalamud/Plugin/Services/IPluginLog.cs +++ b/Dalamud/Plugin/Services/IPluginLog.cs @@ -1,8 +1,8 @@ -using System; - -using Serilog; +using Serilog; using Serilog.Events; +#pragma warning disable CS1573 // See https://github.com/dotnet/roslyn/issues/40325 + namespace Dalamud.Plugin.Services; /// From 57ae2264e3461ba9d4367527fc39586391619d4e Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 16 Sep 2023 18:11:06 -0700 Subject: [PATCH 099/122] Fix SA1502 errors on autoformat - Braces in `{ }` style cause SA1502 to complain. --- .editorconfig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0e4f800e0..0ae30cf95 100644 --- a/.editorconfig +++ b/.editorconfig @@ -57,12 +57,12 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly -dotnet_style_parentheses_in_arithmetic_binary_operators =always_for_clarity:suggestion -dotnet_style_parentheses_in_other_binary_operators =always_for_clarity:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion -dotnet_style_parentheses_in_other_operators=always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = always_for_clarity:silent dotnet_style_object_initializer = false dotnet_style_qualification_for_event = true:suggestion dotnet_style_qualification_for_field = true:suggestion @@ -78,7 +78,7 @@ csharp_space_before_comma = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_comma = true csharp_space_after_cast = false -csharp_space_around_binary_operators = before_and_after +csharp_space_around_binary_operators = before_and_after csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = none @@ -101,7 +101,7 @@ resharper_braces_for_ifelse = required_for_multiline resharper_can_use_global_alias = false resharper_csharp_align_multiline_parameter = true resharper_csharp_align_multiple_declaration = true -resharper_csharp_empty_block_style = together_same_line +resharper_csharp_empty_block_style = multiline resharper_csharp_int_align_comments = true resharper_csharp_new_line_before_while = true resharper_csharp_wrap_after_declaration_lpar = true @@ -133,13 +133,13 @@ resharper_suggest_var_or_type_built_in_types_highlighting = hint resharper_suggest_var_or_type_elsewhere_highlighting = hint resharper_suggest_var_or_type_simple_types_highlighting = hint resharper_unused_auto_property_accessor_global_highlighting = none -csharp_style_deconstructed_variable_declaration=true:silent +csharp_style_deconstructed_variable_declaration = true:silent [*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] indent_style = space indent_size = 4 tab_width = 4 -dotnet_style_parentheses_in_other_operators=always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = always_for_clarity:silent [*.{yaml,yml}] indent_style = space From 08fd4434eaae56d7dae7b6dbdbf7263eb5b0ded0 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 17 Sep 2023 05:41:56 -0700 Subject: [PATCH 100/122] Dalamud Console Window UI-Rework (#1390) Co-authored-by: Kaz Wolfe --- .../Internal/Windows/ConsoleWindow.cs | 584 ++++++++++++------ 1 file changed, 388 insertions(+), 196 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 0febc0fc4..4bd41c025 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Text; +using System.Text.RegularExpressions; using Dalamud.Configuration.Internal; using Dalamud.Game.Command; @@ -13,6 +14,7 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; +using Dalamud.Utility; using ImGuiNET; using Serilog; using Serilog.Events; @@ -27,26 +29,26 @@ internal class ConsoleWindow : Window, IDisposable private readonly List logText = new(); private readonly object renderLock = new(); - private readonly string[] logLevelStrings = new[] { "Verbose", "Debug", "Information", "Warning", "Error", "Fatal" }; - - private List filteredLogText = new(); - private bool autoScroll; - private bool openAtStartup; + private readonly List history = new(); + private readonly List pluginFilters = new(); private bool? lastCmdSuccess; private string commandText = string.Empty; - private string textFilter = string.Empty; - private int levelFilter; - private List sourceFilters = new(); - private bool filterShowUncaughtExceptions = false; - private bool isFiltered = false; + private string selectedSource = "DalamudInternal"; + + private bool filterShowUncaughtExceptions; + private bool showFilterToolbar; + private bool clearLog; + private bool copyLog; + private bool copyMode; + private bool killGameArmed; + private bool autoScroll; + private bool autoOpen; private int historyPos; - private List history = new(); - - private bool killGameArmed = false; + private int copyStart = -1; /// /// Initializes a new instance of the class. @@ -57,16 +59,22 @@ internal class ConsoleWindow : Window, IDisposable var configuration = Service.Get(); this.autoScroll = configuration.LogAutoScroll; - this.openAtStartup = configuration.LogOpenAtStartup; + this.autoOpen = configuration.LogOpenAtStartup; SerilogEventSink.Instance.LogLine += this.OnLogLine; this.Size = new Vector2(500, 400); this.SizeCondition = ImGuiCond.FirstUseEver; + this.SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(600.0f, 200.0f), + MaximumSize = new Vector2(9999.0f, 9999.0f), + }; + this.RespectCloseHotkey = false; } - private List LogEntries => this.isFiltered ? this.filteredLogText : this.logText; + private List FilteredLogEntries { get; set; } = new(); /// public override void OnOpen() @@ -91,10 +99,20 @@ internal class ConsoleWindow : Window, IDisposable lock (this.renderLock) { this.logText.Clear(); - this.filteredLogText.Clear(); + this.FilteredLogEntries.Clear(); + this.clearLog = false; } } + /// + /// Copies the entire log contents to clipboard. + /// + public void CopyLog() + { + ImGui.LogToClipboard(); + this.copyLog = false; + } + /// /// Add a single log line to the display. /// @@ -122,160 +140,15 @@ internal class ConsoleWindow : Window, IDisposable /// public override void Draw() { - // Options menu - if (ImGui.BeginPopup("Options")) - { - var configuration = Service.Get(); + this.DrawOptionsToolbar(); - if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) - { - configuration.LogAutoScroll = this.autoScroll; - configuration.QueueSave(); - } - - if (ImGui.Checkbox("Open at startup", ref this.openAtStartup)) - { - configuration.LogOpenAtStartup = this.openAtStartup; - configuration.QueueSave(); - } - - var prevLevel = (int)EntryPoint.LogLevelSwitch.MinimumLevel; - if (ImGui.Combo("Log Level", ref prevLevel, Enum.GetValues(typeof(LogEventLevel)).Cast().Select(x => x.ToString()).ToArray(), 6)) - { - EntryPoint.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel; - configuration.LogLevel = (LogEventLevel)prevLevel; - configuration.QueueSave(); - } - - ImGui.EndPopup(); - } - - // Filter menu - if (ImGui.BeginPopup("Filters")) - { - if (ImGui.Checkbox("Enabled", ref this.isFiltered)) - { - this.Refilter(); - } - - if (ImGui.InputTextWithHint("##filterText", "Text Filter", ref this.textFilter, 255, ImGuiInputTextFlags.EnterReturnsTrue)) - { - this.Refilter(); - } - - ImGui.TextColored(ImGuiColors.DalamudGrey, "Enter to confirm."); - - if (ImGui.BeginCombo("Levels", this.levelFilter == 0 ? "All Levels..." : "Selected Levels...")) - { - for (var i = 0; i < this.logLevelStrings.Length; i++) - { - if (ImGui.Selectable(this.logLevelStrings[i], ((this.levelFilter >> i) & 1) == 1)) - { - this.levelFilter ^= 1 << i; - this.Refilter(); - } - } - - ImGui.EndCombo(); - } - - // Filter by specific plugin(s) - var sourceNames = Service.Get().InstalledPlugins - .Select(p => p.Manifest.InternalName) - .OrderBy(s => s) - .Prepend("DalamudInternal") - .ToList(); - - var sourcePreviewVal = this.sourceFilters.Count switch - { - 0 => "All sources...", - 1 => "1 source...", - _ => $"{this.sourceFilters.Count} sources...", - }; - var sourceSelectables = sourceNames.Union(this.sourceFilters).ToList(); - if (ImGui.BeginCombo("Sources", sourcePreviewVal)) - { - foreach (var selectable in sourceSelectables) - { - if (ImGui.Selectable(selectable, this.sourceFilters.Contains(selectable))) - { - if (!this.sourceFilters.Contains(selectable)) - { - this.sourceFilters.Add(selectable); - } - else - { - this.sourceFilters.Remove(selectable); - } - - this.Refilter(); - } - } - - ImGui.EndCombo(); - } - - if (ImGui.Checkbox("Always Show Uncaught Exceptions", ref this.filterShowUncaughtExceptions)) - { - this.Refilter(); - } - - ImGui.EndPopup(); - } - - ImGui.SameLine(); - - if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) - ImGui.OpenPopup("Options"); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Options"); - - ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) - ImGui.OpenPopup("Filters"); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Filters"); - - ImGui.SameLine(); - var clear = ImGuiComponents.IconButton(FontAwesomeIcon.Trash); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Clear Log"); - - ImGui.SameLine(); - var copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Copy Log"); - - ImGui.SameLine(); - if (this.killGameArmed) - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.Flushed)) - Process.GetCurrentProcess().Kill(); - } - else - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.Skull)) - this.killGameArmed = true; - } - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Kill game"); + this.DrawFilterToolbar(); ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); - if (clear) - { - this.Clear(); - } + if (this.clearLog) this.Clear(); - if (copy) - { - ImGui.LogToClipboard(); - } + if (this.copyLog) this.CopyLog(); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); @@ -291,27 +164,40 @@ internal class ConsoleWindow : Window, IDisposable var childDrawList = ImGui.GetWindowDrawList(); var childSize = ImGui.GetWindowSize(); - var cursorDiv = ImGuiHelpers.GlobalScale * 92; + var cursorDiv = ImGuiHelpers.GlobalScale * 93; var cursorLogLevel = ImGuiHelpers.GlobalScale * 100; var cursorLogLine = ImGuiHelpers.GlobalScale * 135; lock (this.renderLock) { - clipper.Begin(this.LogEntries.Count); + clipper.Begin(this.FilteredLogEntries.Count); while (clipper.Step()) { for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { - var line = this.LogEntries[i]; + var line = this.FilteredLogEntries[i]; - if (!line.IsMultiline && !copy) + if (!line.IsMultiline && !this.copyLog) ImGui.Separator(); + + if (line.SelectedForCopy) + { + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGrey); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, ImGuiColors.ParsedGrey); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, ImGuiColors.ParsedGrey); + } + else + { + ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level)); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level)); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level)); + } - ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level)); - ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level)); - ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level)); + ImGui.Selectable("###console_null", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns); - ImGui.Selectable("###consolenull", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns); + // This must be after ImGui.Selectable, it uses ImGui.IsItem... functions + this.HandleCopyMode(i, line); + ImGui.SameLine(); ImGui.PopStyleColor(3); @@ -366,12 +252,12 @@ internal class ConsoleWindow : Window, IDisposable } } - ImGui.SetNextItemWidth(ImGui.GetWindowSize().X - 80); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (80.0f * ImGuiHelpers.GlobalScale) - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale)); var getFocus = false; unsafe { - if (ImGui.InputText("##commandbox", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback)) + if (ImGui.InputText("##command_box", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback)) { this.ProcessCommand(); getFocus = true; @@ -387,16 +273,279 @@ internal class ConsoleWindow : Window, IDisposable if (hadColor) ImGui.PopStyleColor(); - if (ImGui.Button("Send")) + if (ImGui.Button("Send", ImGuiHelpers.ScaledVector2(80.0f, 23.0f))) { this.ProcessCommand(); } } + + private void HandleCopyMode(int i, LogEntry line) + { + var selectionChanged = false; + + // If copyStart is -1, it means a drag has not been started yet, let's start one, and select the starting spot. + if (this.copyMode && this.copyStart == -1 && ImGui.IsItemClicked()) + { + this.copyStart = i; + line.SelectedForCopy = !line.SelectedForCopy; + + selectionChanged = true; + } + + // Update the selected range when dragging over entries + if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + { + if (!line.SelectedForCopy) + { + foreach (var index in Enumerable.Range(0, this.FilteredLogEntries.Count)) + { + if (this.copyStart < i) + { + this.FilteredLogEntries[index].SelectedForCopy = index >= this.copyStart && index <= i; + } + else + { + this.FilteredLogEntries[index].SelectedForCopy = index >= i && index <= this.copyStart; + } + } + + selectionChanged = true; + } + } + + // Finish the drag, we should have already marked all dragged entries as selected by now. + if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + this.copyStart = -1; + } + + if (selectionChanged) + { + var allSelectedLines = this.FilteredLogEntries + .Where(entry => entry.SelectedForCopy) + .Select(entry => $"{line.TimeStamp:HH:mm:ss.fff} {this.GetTextForLogEventLevel(entry.Level)} | {entry.Line}"); + + ImGui.SetClipboardText(string.Join("\n", allSelectedLines)); + } + } + + private void DrawOptionsToolbar() + { + var configuration = Service.Get(); + + ImGui.PushItemWidth(150.0f * ImGuiHelpers.GlobalScale); + if (ImGui.BeginCombo("##log_level", $"{EntryPoint.LogLevelSwitch.MinimumLevel}+")) + { + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), value == EntryPoint.LogLevelSwitch.MinimumLevel)) + { + EntryPoint.LogLevelSwitch.MinimumLevel = value; + configuration.LogLevel = value; + configuration.QueueSave(); + this.Refilter(); + } + } + + ImGui.EndCombo(); + } + + ImGui.SameLine(); + + this.autoScroll = configuration.LogAutoScroll; + if (this.DrawToggleButtonWithTooltip("auto_scroll", "Auto-scroll", FontAwesomeIcon.Sync, ref this.autoScroll)) + { + configuration.LogAutoScroll = !configuration.LogAutoScroll; + configuration.QueueSave(); + } + + ImGui.SameLine(); + + this.autoOpen = configuration.LogOpenAtStartup; + if (this.DrawToggleButtonWithTooltip("auto_open", "Open at startup", FontAwesomeIcon.WindowRestore, ref this.autoOpen)) + { + configuration.LogOpenAtStartup = !configuration.LogOpenAtStartup; + configuration.QueueSave(); + } + + ImGui.SameLine(); + + if (this.DrawToggleButtonWithTooltip("show_filters", "Show filter toolbar", FontAwesomeIcon.Search, ref this.showFilterToolbar)) + { + this.showFilterToolbar = !this.showFilterToolbar; + } + + ImGui.SameLine(); + + if (this.DrawToggleButtonWithTooltip("show_uncaught_exceptions", "Show uncaught exception while filtering", FontAwesomeIcon.Bug, ref this.filterShowUncaughtExceptions)) + { + this.filterShowUncaughtExceptions = !this.filterShowUncaughtExceptions; + } + + ImGui.SameLine(); + + if (ImGuiComponents.IconButton("clear_log", FontAwesomeIcon.Trash)) + { + this.clearLog = true; + } + + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Clear Log"); + + ImGui.SameLine(); + + if (this.DrawToggleButtonWithTooltip("copy_mode", "Enable Copy Mode\nRight-click to copy entire log", FontAwesomeIcon.Copy, ref this.copyMode)) + { + this.copyMode = !this.copyMode; + + if (!this.copyMode) + { + foreach (var entry in this.FilteredLogEntries) + { + entry.SelectedForCopy = false; + } + } + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) this.copyLog = true; + + ImGui.SameLine(); + if (this.killGameArmed) + { + if (ImGuiComponents.IconButton(FontAwesomeIcon.ExclamationTriangle)) + Process.GetCurrentProcess().Kill(); + } + else + { + if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop)) + this.killGameArmed = true; + } + + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game"); + + ImGui.SameLine(); + ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - (200.0f * ImGuiHelpers.GlobalScale)); + ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale); + if (ImGui.InputTextWithHint("##global_filter", "regex global filter", ref this.textFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)) + { + this.Refilter(); + } + + if (ImGui.IsItemDeactivatedAfterEdit()) + { + this.Refilter(); + } + } + + private void DrawFilterToolbar() + { + if (!this.showFilterToolbar) return; + + PluginFilterEntry? removalEntry = null; + if (ImGui.BeginTable("plugin_filter_entries", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV)) + { + ImGui.TableSetupColumn("##remove_button", ImGuiTableColumnFlags.WidthFixed, 25.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##source_name", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##log_level", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##filter_text", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton("add_entry", FontAwesomeIcon.Plus)) + { + if (this.pluginFilters.All(entry => entry.Source != this.selectedSource)) + { + this.pluginFilters.Add(new PluginFilterEntry + { + Source = this.selectedSource, + Filter = string.Empty, + Level = LogEventLevel.Debug, + }); + } + + this.Refilter(); + } + + ImGui.TableNextColumn(); + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.BeginCombo("##Sources", this.selectedSource)) + { + var sourceNames = Service.Get().InstalledPlugins + .Select(p => p.Manifest.InternalName) + .OrderBy(s => s) + .Prepend("DalamudInternal") + .ToList(); + + foreach (var selectable in sourceNames) + { + if (ImGui.Selectable(selectable, this.selectedSource == selectable)) + { + this.selectedSource = selectable; + } + } + + ImGui.EndCombo(); + } + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + + foreach (var entry in this.pluginFilters) + { + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton($"remove{entry.Source}", FontAwesomeIcon.Trash)) + { + removalEntry = entry; + } + + ImGui.TableNextColumn(); + ImGui.Text(entry.Source); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.BeginCombo($"##levels{entry.Source}", $"{entry.Level}+")) + { + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), value == entry.Level)) + { + entry.Level = value; + this.Refilter(); + } + } + + ImGui.EndCombo(); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var entryFilter = entry.Filter; + if (ImGui.InputTextWithHint($"##filter{entry.Source}", $"{entry.Source} regex filter", ref entryFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)) + { + entry.Filter = entryFilter; + this.Refilter(); + } + + if (ImGui.IsItemDeactivatedAfterEdit()) this.Refilter(); + } + + ImGui.EndTable(); + } + + if (removalEntry is { } toRemove) + { + this.pluginFilters.Remove(toRemove); + this.Refilter(); + } + } private void ProcessCommand() { try { + if (this.commandText is['/', ..]) + { + this.commandText = this.commandText[1..]; + } + this.historyPos = -1; for (var i = this.history.Count - 1; i >= 0; i--) { @@ -409,7 +558,7 @@ internal class ConsoleWindow : Window, IDisposable this.history.Add(this.commandText); - if (this.commandText == "clear" || this.commandText == "cls") + if (this.commandText is "clear" or "cls") { this.Clear(); return; @@ -446,7 +595,8 @@ internal class ConsoleWindow : Window, IDisposable // TODO: Improve this, add partial completion // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484 - var candidates = Service.Get().Commands.Where(x => x.Key.Contains("/" + words[0])) + var candidates = Service.Get().Commands + .Where(x => x.Key.Contains("/" + words[0])) .ToList(); if (candidates.Count > 0) { @@ -455,6 +605,7 @@ internal class ConsoleWindow : Window, IDisposable } break; + case ImGuiInputTextFlags.CallbackHistory: var prevPos = this.historyPos; @@ -503,7 +654,7 @@ internal class ConsoleWindow : Window, IDisposable TimeStamp = logEvent.Timestamp, HasException = logEvent.Exception != null, }; - + // TODO (v9): Remove SourceContext property check. if (logEvent.Properties.ContainsKey("Dalamud.ModuleName")) { @@ -518,37 +669,49 @@ internal class ConsoleWindow : Window, IDisposable this.logText.Add(entry); - if (!this.isFiltered) - return; - if (this.IsFilterApplicable(entry)) - this.filteredLogText.Add(entry); + this.FilteredLogEntries.Add(entry); } private bool IsFilterApplicable(LogEntry entry) { - if (this.levelFilter > 0 && ((this.levelFilter >> (int)entry.Level) & 1) == 0) + // If this entry is below a newly set minimum level, fail it + if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level) return false; - + // Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught) // After log levels because uncaught exceptions should *never* fall below Error. if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null) return true; - if (this.sourceFilters.Count > 0 && !this.sourceFilters.Contains(entry.Source)) - return false; + // If we have a global filter, check that first + if (!this.textFilter.IsNullOrEmpty()) + { + // Someone will definitely try to just text filter a source without using the actual filters, should allow that. + var matchesSource = entry.Source is not null && Regex.IsMatch(entry.Source, this.textFilter, RegexOptions.IgnoreCase); + var matchesContent = Regex.IsMatch(entry.Line, this.textFilter, RegexOptions.IgnoreCase); - if (!string.IsNullOrEmpty(this.textFilter) && !entry.Line.Contains(this.textFilter)) - return false; + return matchesSource || matchesContent; + } - return true; + // If this entry has a filter, check the filter + if (this.pluginFilters.FirstOrDefault(filter => string.Equals(filter.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase)) is { } filterEntry) + { + var allowedLevel = filterEntry.Level <= entry.Level; + var matchesContent = filterEntry.Filter.IsNullOrEmpty() || Regex.IsMatch(entry.Line, filterEntry.Filter, RegexOptions.IgnoreCase); + + return allowedLevel && matchesContent; + } + + // else we couldn't find a filter for this entry, if we have any filters, we need to block this entry. + return !this.pluginFilters.Any(); } private void Refilter() { lock (this.renderLock) { - this.filteredLogText = this.logText.Where(this.IsFilterApplicable).ToList(); + this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList(); } } @@ -579,22 +742,51 @@ internal class ConsoleWindow : Window, IDisposable this.HandleLogLine(logEvent.Line, logEvent.LogEvent); } + private bool DrawToggleButtonWithTooltip(string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState) + { + var result = false; + + var buttonEnabled = enabledState; + if (buttonEnabled) ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.25f }); + if (ImGuiComponents.IconButton(buttonId, icon)) + { + result = true; + } + + if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); + + if (buttonEnabled) ImGui.PopStyleColor(); + + return result; + } + private class LogEntry { - public string Line { get; set; } + public string Line { get; init; } = string.Empty; - public LogEventLevel Level { get; set; } + public LogEventLevel Level { get; init; } - public DateTimeOffset TimeStamp { get; set; } + public DateTimeOffset TimeStamp { get; init; } - public bool IsMultiline { get; set; } + public bool IsMultiline { get; init; } /// /// Gets or sets the system responsible for generating this log entry. Generally will be a plugin's /// InternalName. /// public string? Source { get; set; } + + public bool SelectedForCopy { get; set; } - public bool HasException { get; set; } + public bool HasException { get; init; } + } + + private class PluginFilterEntry + { + public string Source { get; init; } = string.Empty; + + public string Filter { get; set; } = string.Empty; + + public LogEventLevel Level { get; set; } } } From 2c23e6fdb33db0dad89a3a93b02073f19d77169f Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sun, 17 Sep 2023 06:56:49 -0700 Subject: [PATCH 101/122] [v9] Move GPose check to ClientState (#1378) --- Dalamud/Game/ClientState/ClientState.cs | 3 +++ Dalamud/Interface/UiBuilder.cs | 18 +++--------------- Dalamud/Plugin/Services/IClientState.cs | 5 +++++ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index fed0ec3c4..6817523af 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -102,6 +102,9 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState /// public bool IsPvPExcludingDen { get; private set; } + /// + public bool IsGPosing => GameMain.IsInGPose(); + /// /// Gets client state address resolver. /// diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index b440a0705..95ee28f56 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; +using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; @@ -179,20 +180,6 @@ public sealed class UiBuilder : IDisposable } } - /// - /// Gets a value indicating whether or not gpose is active. - /// - public bool GposeActive - { - get - { - var condition = Service.GetNullable(); - if (condition == null) - return false; - return condition[ConditionFlag.WatchingCutscene]; - } - } - /// /// Gets a value indicating whether this plugin should modify the game's interface at this time. /// @@ -448,6 +435,7 @@ public sealed class UiBuilder : IDisposable { this.hitchDetector.Start(); + var clientState = Service.Get(); var configuration = Service.Get(); var gameGui = Service.GetNullable(); if (gameGui == null) @@ -457,7 +445,7 @@ public sealed class UiBuilder : IDisposable !(this.DisableUserUiHide || this.DisableAutomaticUiHide)) || (this.CutsceneActive && configuration.ToggleUiHideDuringCutscenes && !(this.DisableCutsceneUiHide || this.DisableAutomaticUiHide)) || - (this.GposeActive && configuration.ToggleUiHideDuringGpose && + (clientState.IsGPosing && configuration.ToggleUiHideDuringGpose && !(this.DisableGposeUiHide || this.DisableAutomaticUiHide))) { if (!this.lastFrameUiHideState) diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index d66db9cc9..881cad841 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -73,4 +73,9 @@ public interface IClientState /// Gets a value indicating whether or not the user is playing PvP, excluding the Wolves' Den. /// public bool IsPvPExcludingDen { get; } + + /// + /// Gets a value indicating whether the client is currently in Group Pose (GPose) mode. + /// + public bool IsGPosing { get; } } From 452cf7813f70c71e9365f640273f46154491bb97 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 17 Sep 2023 07:01:11 -0700 Subject: [PATCH 102/122] Data Window ui rework (#1376) --- .../Internal/Windows/Data/DataKindEnum.cs | 163 --------------- .../Internal/Windows/Data/DataWindow.cs | 186 +++++++++++------- .../Windows/Data/IDataWindowWidget.cs | 21 +- .../Data/Widgets/AddonInspectorWidget.cs | 5 +- .../Windows/Data/Widgets/AddonWidget.cs | 5 +- .../Windows/Data/Widgets/AddressesWidget.cs | 5 +- .../Windows/Data/Widgets/AetherytesWidget.cs | 7 +- .../Data/Widgets/AtkArrayDataBrowserWidget.cs | 7 +- .../Windows/Data/Widgets/BuddyListWidget.cs | 7 +- .../Windows/Data/Widgets/CommandWidget.cs | 5 +- .../Windows/Data/Widgets/ConditionWidget.cs | 7 +- .../Data/Widgets/ConfigurationWidget.cs | 5 +- .../Windows/Data/Widgets/DataShareWidget.cs | 5 +- .../Windows/Data/Widgets/DtrBarWidget.cs | 5 +- .../Windows/Data/Widgets/FateTableWidget.cs | 5 +- .../Windows/Data/Widgets/FlyTextWidget.cs | 5 +- .../Data/Widgets/FontAwesomeTestWidget.cs | 5 +- .../Windows/Data/Widgets/GamepadWidget.cs | 5 +- .../Windows/Data/Widgets/GaugeWidget.cs | 5 +- .../Windows/Data/Widgets/HookWidget.cs | 5 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 5 +- .../Windows/Data/Widgets/KeyStateWidget.cs | 5 +- .../Data/Widgets/NetworkMonitorWidget.cs | 5 +- .../Windows/Data/Widgets/ObjectTableWidget.cs | 5 +- .../Windows/Data/Widgets/PartyListWidget.cs | 5 +- .../Windows/Data/Widgets/PluginIpcWidget.cs | 5 +- .../Windows/Data/Widgets/SeFontTestWidget.cs | 5 +- .../Data/Widgets/ServerOpcodeWidget.cs | 5 +- .../Windows/Data/Widgets/StartInfoWidget.cs | 5 +- .../Windows/Data/Widgets/TargetWidget.cs | 5 +- .../Data/Widgets/TaskSchedulerWidget.cs | 5 +- .../Windows/Data/Widgets/TexWidget.cs | 5 +- .../Windows/Data/Widgets/ToastWidget.cs | 5 +- .../Windows/Data/Widgets/UIColorWidget.cs | 5 +- 34 files changed, 257 insertions(+), 276 deletions(-) delete mode 100644 Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs deleted file mode 100644 index d7c4eb095..000000000 --- a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ReSharper disable InconsistentNaming // Naming is suppressed so we can replace '_' with ' ' -namespace Dalamud.Interface.Internal.Windows; - -/// -/// Enum representing a DataKind for the Data Window. -/// -internal enum DataKind -{ - /// - /// Server Opcode Display. - /// - Server_OpCode, - - /// - /// Address. - /// - Address, - - /// - /// Object Table. - /// - Object_Table, - - /// - /// Fate Table. - /// - Fate_Table, - - /// - /// SE Font Test. - /// - SE_Font_Test, - - /// - /// FontAwesome Test. - /// - FontAwesome_Test, - - /// - /// Party List. - /// - Party_List, - - /// - /// Buddy List. - /// - Buddy_List, - - /// - /// Plugin IPC Test. - /// - Plugin_IPC, - - /// - /// Player Condition. - /// - Condition, - - /// - /// Gauge. - /// - Gauge, - - /// - /// Command. - /// - Command, - - /// - /// Addon. - /// - Addon, - - /// - /// Addon Inspector. - /// - Addon_Inspector, - - /// - /// AtkArrayData Browser. - /// - AtkArrayData_Browser, - - /// - /// StartInfo. - /// - StartInfo, - - /// - /// Target. - /// - Target, - - /// - /// Toast. - /// - Toast, - - /// - /// Fly Text. - /// - FlyText, - - /// - /// ImGui. - /// - ImGui, - - /// - /// Tex. - /// - Tex, - - /// - /// KeyState. - /// - KeyState, - - /// - /// GamePad. - /// - Gamepad, - - /// - /// Configuration. - /// - Configuration, - - /// - /// Task Scheduler. - /// - TaskSched, - - /// - /// Hook. - /// - Hook, - - /// - /// Aetherytes. - /// - Aetherytes, - - /// - /// DTR Bar. - /// - Dtr_Bar, - - /// - /// UIColor. - /// - UIColor, - - /// - /// Data Share. - /// - Data_Share, - - /// - /// Network Monitor. - /// - Network_Monitor, -} diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 9d8dc1e93..05d5bff3f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -51,26 +50,24 @@ internal class DataWindow : Window new NetworkMonitorWidget(), }; - private readonly Dictionary dataKindNames = new(); + private readonly IOrderedEnumerable orderedModules; private bool isExcept; - private DataKind currentKind; - + private bool selectionCollapsed; + private IDataWindowWidget currentWidget; + /// /// Initializes a new instance of the class. /// public DataWindow() - : base("Dalamud Data") + : base("Dalamud Data", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { - this.Size = new Vector2(500, 500); + this.Size = new Vector2(400, 300); this.SizeCondition = ImGuiCond.FirstUseEver; this.RespectCloseHotkey = false; - - foreach (var dataKind in Enum.GetValues()) - { - this.dataKindNames[dataKind] = dataKind.ToString().Replace("_", " "); - } + this.orderedModules = this.modules.OrderBy(module => module.DisplayName); + this.currentWidget = this.orderedModules.First(); this.Load(); } @@ -94,24 +91,9 @@ internal class DataWindow : Window if (string.IsNullOrEmpty(dataKind)) return; - dataKind = dataKind switch + if (this.modules.FirstOrDefault(module => module.IsWidgetCommand(dataKind)) is { } targetModule) { - "ai" => "Addon Inspector", - "at" => "Object Table", // Actor Table - "ot" => "Object Table", - "uic" => "UIColor", - _ => dataKind, - }; - - dataKind = dataKind.Replace(" ", string.Empty).ToLower(); - - var matched = Enum - .GetValues() - .FirstOrDefault(kind => Enum.GetName(kind)?.Replace("_", string.Empty).ToLower() == dataKind); - - if (matched != default) - { - this.currentKind = matched; + this.currentWidget = targetModule; } else { @@ -124,59 +106,113 @@ internal class DataWindow : Window /// public override void Draw() { - if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync)) this.Load(); - if (ImGui.IsItemHovered()) ImGui.SetTooltip("Force Reload"); - ImGui.SameLine(); - var copy = ImGuiComponents.IconButton("copyAll", FontAwesomeIcon.ClipboardList); - if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy All"); - ImGui.SameLine(); - - ImGui.SetNextItemWidth(275.0f * ImGuiHelpers.GlobalScale); - if (ImGui.BeginCombo("Data Kind", this.dataKindNames[this.currentKind])) + // Only draw the widget contents if the selection pane is collapsed. + if (this.selectionCollapsed) { - foreach (var module in this.modules.OrderBy(module => this.dataKindNames[module.DataKind])) + this.DrawContents(); + return; + } + + if (ImGui.BeginTable("XlData_Table", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.Resizable)) + { + ImGui.TableSetupColumn("##SelectionColumn", ImGuiTableColumnFlags.WidthFixed, 200.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##ContentsColumn", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + this.DrawSelection(); + + ImGui.TableNextColumn(); + this.DrawContents(); + + ImGui.EndTable(); + } + } + + private void DrawSelection() + { + if (ImGui.BeginChild("XlData_SelectionPane", ImGui.GetContentRegionAvail())) + { + if (ImGui.BeginListBox("WidgetSelectionListbox", ImGui.GetContentRegionAvail())) { - if (ImGui.Selectable(this.dataKindNames[module.DataKind], this.currentKind == module.DataKind)) + foreach (var widget in this.orderedModules) { - this.currentKind = module.DataKind; + if (ImGui.Selectable(widget.DisplayName, this.currentWidget == widget)) + { + this.currentWidget = widget; + } + } + + ImGui.EndListBox(); + } + } + + ImGui.EndChild(); + } + + private void DrawContents() + { + if (ImGui.BeginChild("XlData_ContentsPane", ImGui.GetContentRegionAvail())) + { + if (ImGuiComponents.IconButton("collapse-expand", this.selectionCollapsed ? FontAwesomeIcon.ArrowRight : FontAwesomeIcon.ArrowLeft)) + { + this.selectionCollapsed = !this.selectionCollapsed; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip($"{(this.selectionCollapsed ? "Expand" : "Collapse")} selection pane"); + } + + ImGui.SameLine(); + + if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync)) + { + this.Load(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Force Reload"); + } + + ImGui.SameLine(); + + var copy = ImGuiComponents.IconButton("copyAll", FontAwesomeIcon.ClipboardList); + + ImGuiHelpers.ScaledDummy(10.0f); + + if (ImGui.BeginChild("XlData_WidgetContents", ImGui.GetContentRegionAvail())) + { + if (copy) + ImGui.LogToClipboard(); + + try + { + if (this.currentWidget is { Ready: true }) + { + this.currentWidget.Draw(); + } + else + { + ImGui.TextUnformatted("Data not ready."); + } + + this.isExcept = false; + } + catch (Exception ex) + { + if (!this.isExcept) + { + Log.Error(ex, "Could not draw data"); + } + + this.isExcept = true; + + ImGui.TextUnformatted(ex.ToString()); } } - - ImGui.EndCombo(); - } - - ImGuiHelpers.ScaledDummy(10.0f); - ImGui.BeginChild("scrolling", Vector2.Zero, false, ImGuiWindowFlags.HorizontalScrollbar); - - if (copy) - ImGui.LogToClipboard(); - - try - { - var selectedWidget = this.modules.FirstOrDefault(dataWindowWidget => dataWindowWidget.DataKind == this.currentKind); - - if (selectedWidget is { Ready: true }) - { - selectedWidget.Draw(); - } - else - { - ImGui.TextUnformatted("Data not ready."); - } - - this.isExcept = false; - } - catch (Exception ex) - { - if (!this.isExcept) - { - Log.Error(ex, "Could not draw data"); - } - - this.isExcept = true; - - ImGui.TextUnformatted(ex.ToString()); + ImGui.EndChild(); } ImGui.EndChild(); diff --git a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs index ebbdfff83..0e12e4c51 100644 --- a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs @@ -1,4 +1,7 @@ -namespace Dalamud.Interface.Internal.Windows; +using System; +using System.Linq; + +namespace Dalamud.Interface.Internal.Windows; /// /// Class representing a date window entry. @@ -6,9 +9,14 @@ internal interface IDataWindowWidget { /// - /// Gets the Data Kind for this data window module. + /// Gets the command strings that can be used to open the data window directly to this module. /// - DataKind DataKind { get; init; } + string[]? CommandShortcuts { get; init; } + + /// + /// Gets the display name for this module. + /// + string DisplayName { get; init; } /// /// Gets or sets a value indicating whether this data window module is ready. @@ -24,4 +32,11 @@ internal interface IDataWindowWidget /// Draws this data window module. /// void Draw(); + + /// + /// Helper method to check if this widget should be activated by the input command. + /// + /// The command being run. + /// true if this module should be activated by the input command. + bool IsWidgetCommand(string command) => this.CommandShortcuts?.Any(shortcut => string.Equals(shortcut, command, StringComparison.InvariantCultureIgnoreCase)) ?? false; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs index 977037cc5..b39dfcc4f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs @@ -8,7 +8,10 @@ internal class AddonInspectorWidget : IDataWindowWidget private UiDebug? addonInspector; /// - public DataKind DataKind { get; init; } = DataKind.Addon_Inspector; + public string[]? CommandShortcuts { get; init; } = { "ai", "addoninspector" }; + + /// + public string DisplayName { get; init; } = "Addon Inspector"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs index b26b7e311..8d11e6285 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs @@ -15,7 +15,10 @@ internal unsafe class AddonWidget : IDataWindowWidget private nint findAgentInterfacePtr; /// - public DataKind DataKind { get; init; } = DataKind.Addon; + public string DisplayName { get; init; } = "Addon"; + + /// + public string[]? CommandShortcuts { get; init; } /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs index 606fedadd..2beea905e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs @@ -14,7 +14,10 @@ internal class AddressesWidget : IDataWindowWidget private nint sigResult = nint.Zero; /// - public DataKind DataKind { get; init; } = DataKind.Address; + public string[]? CommandShortcuts { get; init; } = { "address" }; + + /// + public string DisplayName { get; init; } = "Addresses"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs index cc4771847..fd4a76544 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs @@ -9,10 +9,13 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class AetherytesWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Aetherytes; + public bool Ready { get; set; } /// - public bool Ready { get; set; } + public string[]? CommandShortcuts { get; init; } = { "aetherytes" }; + + /// + public string DisplayName { get; init; } = "Aetherytes"; /// public void Load() diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs index df98f99a6..9aac779b3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs @@ -12,10 +12,13 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.AtkArrayData_Browser; + public bool Ready { get; set; } /// - public bool Ready { get; set; } + public string[]? CommandShortcuts { get; init; } = { "atkarray" }; + + /// + public string DisplayName { get; init; } = "Atk Array Data"; /// public void Load() diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs index 2aeb9d10d..664b4205e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs @@ -12,10 +12,13 @@ internal class BuddyListWidget : IDataWindowWidget private bool resolveGameData; /// - public DataKind DataKind { get; init; } = DataKind.Buddy_List; + public bool Ready { get; set; } /// - public bool Ready { get; set; } + public string[]? CommandShortcuts { get; init; } = { "buddy", "buddylist" }; + + /// + public string DisplayName { get; init; } = "Buddy List"; /// public void Load() diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs index e415431ba..c7f6564d1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class CommandWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Command; + public string[]? CommandShortcuts { get; init; } = { "command" }; + + /// + public string DisplayName { get; init; } = "Command"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs index a5224589f..be19d7ae2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs @@ -9,10 +9,13 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class ConditionWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Condition; + public bool Ready { get; set; } /// - public bool Ready { get; set; } + public string[]? CommandShortcuts { get; init; } = { "condition" }; + + /// + public string DisplayName { get; init; } = "Condition"; /// public void Load() diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs index 3922f22b7..5b85eb814 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class ConfigurationWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Configuration; + public string[]? CommandShortcuts { get; init; } = { "config", "configuration" }; + + /// + public string DisplayName { get; init; } = "Configuration"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index ec7124042..d89be3357 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class DataShareWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Data_Share; + public string[]? CommandShortcuts { get; init; } = { "datashare" }; + + /// + public string DisplayName { get; init; } = "Data Share"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs index 6d3a67e1a..125d6dbbf 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs @@ -14,7 +14,10 @@ internal class DtrBarWidget : IDataWindowWidget private DtrBarEntry? dtrTest3; /// - public DataKind DataKind { get; init; } = DataKind.Dtr_Bar; + public string[]? CommandShortcuts { get; init; } = { "dtr", "dtrbar" }; + + /// + public string DisplayName { get; init; } = "DTR Bar"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs index 779032f1d..ca77f089f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs @@ -11,7 +11,10 @@ internal class FateTableWidget : IDataWindowWidget private bool resolveGameData; /// - public DataKind DataKind { get; init; } = DataKind.Fate_Table; + public string[]? CommandShortcuts { get; init; } = { "fate", "fatetable" }; + + /// + public string DisplayName { get; init; } = "Fate Table"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs index 99c1a3e02..ff937996e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs @@ -22,7 +22,10 @@ internal class FlyTextWidget : IDataWindowWidget private Vector4 flyColor = new(1, 0, 0, 1); /// - public DataKind DataKind { get; init; } = DataKind.FlyText; + public string[]? CommandShortcuts { get; init; } = { "flytext" }; + + /// + public string DisplayName { get; init; } = "Fly Text"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 1ed5e9e83..036ea7000 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -20,7 +20,10 @@ internal class FontAwesomeTestWidget : IDataWindowWidget private bool iconSearchChanged = true; /// - public DataKind DataKind { get; init; } = DataKind.FontAwesome_Test; + public string[]? CommandShortcuts { get; init; } = { "fa", "fatest", "fontawesome" }; + + /// + public string DisplayName { get; init; } = "Font Awesome Test"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs index 1a4408d53..b20e0132e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs @@ -11,7 +11,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class GamepadWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Gamepad; + public string[]? CommandShortcuts { get; init; } = { "gamepad", "controller" }; + + /// + public string DisplayName { get; init; } = "Gamepad"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs index 02862b33d..dee7999ee 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs @@ -12,7 +12,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class GaugeWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Gauge; + public string[]? CommandShortcuts { get; init; } = { "gauge", "jobgauge", "job" }; + + /// + public string DisplayName { get; init; } = "Job Gauge"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index aa565b1e6..d5c566e52 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -23,8 +23,11 @@ internal class HookWidget : IDataWindowWidget NativeFunctions.MessageBoxType type); /// - public DataKind DataKind { get; init; } = DataKind.Hook; + public string DisplayName { get; init; } = "Hook"; + /// + public string[]? CommandShortcuts { get; init; } = { "hook" }; + /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 8afce718f..311004f2d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -12,7 +12,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class ImGuiWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.ImGui; + public string[]? CommandShortcuts { get; init; } = { "imgui" }; + + /// + public string DisplayName { get; init; } = "ImGui"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs index accc48b4b..ce072abc4 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs @@ -10,7 +10,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class KeyStateWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.KeyState; + public string[]? CommandShortcuts { get; init; } = { "keystate" }; + + /// + public string DisplayName { get; init; } = "KeyState"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 01d0b1759..d1c0150dc 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -54,7 +54,10 @@ internal class NetworkMonitorWidget : IDataWindowWidget } /// - public DataKind DataKind { get; init; } = DataKind.Network_Monitor; + public string[]? CommandShortcuts { get; init; } = { "network", "netmon", "networkmonitor" }; + + /// + public string DisplayName { get; init; } = "Network Monitor"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index dd41315f2..42ce3ced6 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -19,8 +19,11 @@ internal class ObjectTableWidget : IDataWindowWidget private float maxCharaDrawDistance = 20.0f; /// - public DataKind DataKind { get; init; } = DataKind.Object_Table; + public string[]? CommandShortcuts { get; init; } = { "ot", "objecttable" }; + /// + public string DisplayName { get; init; } = "Object Table"; + /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs index c5ac1fb8f..369fd7620 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs @@ -12,7 +12,10 @@ internal class PartyListWidget : IDataWindowWidget private bool resolveGameData; /// - public DataKind DataKind { get; init; } = DataKind.Party_List; + public string[]? CommandShortcuts { get; init; } = { "partylist", "party" }; + + /// + public string DisplayName { get; init; } = "Party List"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs index 9aae9bba3..b89ead526 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs @@ -19,7 +19,10 @@ internal class PluginIpcWidget : IDataWindowWidget private string callGateResponse = string.Empty; /// - public DataKind DataKind { get; init; } = DataKind.Plugin_IPC; + public string[]? CommandShortcuts { get; init; } = { "ipc" }; + + /// + public string DisplayName { get; init; } = "Plugin IPC"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs index a642c439d..89dc5735a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class SeFontTestWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.SE_Font_Test; + public string[]? CommandShortcuts { get; init; } = { "sefont", "sefonttest" }; + + /// + public string DisplayName { get; init; } = "SeFont Test"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs index f414e0957..6adf02b3d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs @@ -12,7 +12,10 @@ internal class ServerOpcodeWidget : IDataWindowWidget private string? serverOpString; /// - public DataKind DataKind { get; init; } = DataKind.Server_OpCode; + public string[]? CommandShortcuts { get; init; } = { "opcode", "serveropcode" }; + + /// + public string DisplayName { get; init; } = "Server Opcode"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs index 656efe388..e635b55e0 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class StartInfoWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.StartInfo; + public string[]? CommandShortcuts { get; init; } = { "startinfo" }; + + /// + public string DisplayName { get; init; } = "Start Info"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index f33e67f70..39c6d5b18 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -13,7 +13,10 @@ internal class TargetWidget : IDataWindowWidget private bool resolveGameData; /// - public DataKind DataKind { get; init; } = DataKind.Target; + public string[]? CommandShortcuts { get; init; } = { "target" }; + + /// + public string DisplayName { get; init; } = "Target"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index 7d91cd154..35d449443 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -20,7 +20,10 @@ internal class TaskSchedulerWidget : IDataWindowWidget private CancellationTokenSource taskSchedulerCancelSource = new(); /// - public DataKind DataKind { get; init; } = DataKind.TaskSched; + public string[]? CommandShortcuts { get; init; } = { "tasksched", "taskscheduler" }; + + /// + public string DisplayName { get; init; } = "Task Scheduler"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 5ad5868c3..9c1f93b0b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -28,7 +28,10 @@ internal class TexWidget : IDataWindowWidget private Vector2 inputTexScale = Vector2.Zero; /// - public DataKind DataKind { get; init; } = DataKind.Tex; + public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; + + /// + public string DisplayName { get; init; } = "Tex"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs index c75230e73..336312e87 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs @@ -19,7 +19,10 @@ internal class ToastWidget : IDataWindowWidget private bool questToastCheckmark; /// - public DataKind DataKind { get; init; } = DataKind.Toast; + public string[]? CommandShortcuts { get; init; } = { "toast" }; + + /// + public string DisplayName { get; init; } = "Toast"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index 1d0ccdce6..d2d480fff 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -12,7 +12,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class UIColorWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.UIColor; + public string[]? CommandShortcuts { get; init; } = { "uicolor" }; + + /// + public string DisplayName { get; init; } = "UIColor"; /// public bool Ready { get; set; } From 428e1afefd8e7e9ace99eef506f009f82bb93213 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 20:11:34 +0200 Subject: [PATCH 103/122] fix warnings --- Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index e3ea74f2d..41a8ba56a 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; + using Dalamud.Game.Gui.PartyFinder.Internal; using Dalamud.Game.Gui.PartyFinder.Types; using Dalamud.Hooking; From ab9b7e1602192b0cfdf394d782dd1bd128ea3f9d Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 20:35:40 +0200 Subject: [PATCH 104/122] chore: add data widget for servicecontainer status, remove serveropcode data widget --- .../Internal/Windows/Data/DataWindow.cs | 2 +- .../Data/Widgets/ServerOpcodeWidget.cs | 40 ------------- .../Windows/Data/Widgets/ServicesWidget.cs | 59 +++++++++++++++++++ Dalamud/IoC/Internal/ServiceContainer.cs | 10 ++++ 4 files changed, 70 insertions(+), 41 deletions(-) delete mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 4c446cacd..1363c6abe 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -19,7 +19,7 @@ internal class DataWindow : Window { private readonly IDataWindowWidget[] modules = { - new ServerOpcodeWidget(), + new ServicesWidget(), new AddressesWidget(), new ObjectTableWidget(), new FateTableWidget(), diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs deleted file mode 100644 index b23f3961e..000000000 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Dalamud.Data; -using ImGuiNET; -using Newtonsoft.Json; - -namespace Dalamud.Interface.Internal.Windows.Data.Widgets; - -/// -/// Widget to display the currently set server opcodes. -/// -internal class ServerOpcodeWidget : IDataWindowWidget -{ - private string? serverOpString; - - /// - public string[]? CommandShortcuts { get; init; } = { "opcode", "serveropcode" }; - - /// - public string DisplayName { get; init; } = "Server Opcode"; - - /// - public bool Ready { get; set; } - - /// - public void Load() - { - var dataManager = Service.Get(); - - if (dataManager.IsDataReady) - { - this.serverOpString = JsonConvert.SerializeObject(dataManager.ServerOpCodes, Formatting.Indented); - this.Ready = true; - } - } - - /// - public void Draw() - { - ImGui.TextUnformatted(this.serverOpString ?? "serverOpString not initialized"); - } -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs new file mode 100644 index 000000000..49f3c1b90 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -0,0 +1,59 @@ +using System.Linq; + +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.IoC.Internal; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget for displaying start info. +/// +internal class ServicesWidget : IDataWindowWidget +{ + /// + public string[]? CommandShortcuts { get; init; } = { "services" }; + + /// + public string DisplayName { get; init; } = "Service Container"; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var container = Service.Get(); + + foreach (var instance in container.Instances) + { + var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key); + var isPublic = instance.Key.IsPublic; + + ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})"); + + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) + { + ImGui.Text(hasInterface + ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" + : "\t => NO INTERFACE!!!"); + } + + if (isPublic) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.Text("\t => PUBLIC!!!"); + } + + ImGuiHelpers.ScaledDummy(2); + } + } +} diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index db748303e..a82440029 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -29,6 +29,16 @@ internal class ServiceContainer : IServiceProvider, IServiceType public ServiceContainer() { } + + /// + /// Gets a dictionary of all registered instances. + /// + public IReadOnlyDictionary Instances => this.instances; + + /// + /// Gets a dictionary mapping interfaces to their implementations. + /// + public IReadOnlyDictionary InterfaceToTypeMap => this.interfaceToTypeMap; /// /// Register a singleton object of any type into the current IOC container. From 5809cf5d7c30926f57e16c17466169897919d945 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 21:09:00 +0200 Subject: [PATCH 105/122] chore: make all services with interfaces internal --- Dalamud/Data/DataManager.cs | 2 +- Dalamud/Game/BaseAddressResolver.cs | 2 +- .../Game/ClientState/Aetherytes/AetheryteList.cs | 4 ++-- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 4 ++-- Dalamud/Game/ClientState/ClientState.cs | 5 ++--- .../ClientState/ClientStateAddressResolver.cs | 2 +- Dalamud/Game/ClientState/Conditions/Condition.cs | 4 +--- Dalamud/Game/ClientState/Fates/FateTable.cs | 4 ++-- Dalamud/Game/ClientState/GamePad/GamepadState.cs | 2 +- Dalamud/Game/ClientState/JobGauge/JobGauges.cs | 2 +- Dalamud/Game/ClientState/Keys/KeyState.cs | 2 +- Dalamud/Game/ClientState/Objects/ObjectTable.cs | 4 ++-- Dalamud/Game/ClientState/Objects/TargetManager.cs | 2 +- Dalamud/Game/ClientState/Party/PartyList.cs | 4 ++-- Dalamud/Game/Command/CommandManager.cs | 2 +- Dalamud/Game/Config/GameConfig.cs | 2 +- Dalamud/Game/Config/GameConfigAddressResolver.cs | 2 +- Dalamud/Game/DutyState/DutyState.cs | 2 +- .../Game/DutyState/DutyStateAddressResolver.cs | 2 +- Dalamud/Game/Framework.cs | 15 +++++++-------- Dalamud/Game/FrameworkAddressResolver.cs | 2 +- Dalamud/Game/GameLifecycle.cs | 2 +- Dalamud/Game/Gui/ChatGuiAddressResolver.cs | 4 +--- Dalamud/Game/Gui/Dtr/DtrBar.cs | 7 +++---- .../Game/Gui/FlyText/FlyTextGuiAddressResolver.cs | 4 +--- .../Gui/PartyFinder/PartyFinderAddressResolver.cs | 4 +--- .../Gui/PartyFinder/Types/JobFlagsExtensions.cs | 6 +++--- Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs | 4 +--- .../Game/Internal/DXGI/SwapChainSigResolver.cs | 3 +-- .../Game/Internal/DXGI/SwapChainVtableResolver.cs | 3 +-- Dalamud/Game/Libc/LibcFunction.cs | 2 +- Dalamud/Game/Libc/LibcFunctionAddressResolver.cs | 2 +- .../Game/Network/GameNetworkAddressResolver.cs | 4 +--- Dalamud/Game/SigScanner.cs | 2 +- Dalamud/Game/Text/SeStringHandling/Payload.cs | 14 ++++++++------ Dalamud/Interface/Internal/TextureManager.cs | 2 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 3 ++- Dalamud/Logging/Internal/TaskTracker.cs | 4 ++-- .../Internal/Profiles/ProfileCommandHandler.cs | 6 +++--- Dalamud/Plugin/Services/IFramework.cs | 7 +------ 40 files changed, 67 insertions(+), 86 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index fb167283f..809726684 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -26,7 +26,7 @@ namespace Dalamud.Data; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class DataManager : IDisposable, IServiceType, IDataManager +internal sealed class DataManager : IDisposable, IServiceType, IDataManager { private readonly Thread luminaResourceThread; private readonly CancellationTokenSource luminaCancellationTokenSource; diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index 24e7dffe8..9935aac7b 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game; /// /// Base memory address resolver. /// -public abstract class BaseAddressResolver +internal abstract class BaseAddressResolver { /// /// Gets a list of memory addresses that were found, to list in /xldata. diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index 17b468d70..e6af6e1df 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.ClientState.Aetherytes; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList +internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList { [ServiceManager.ServiceDependency] private readonly ClientState clientState = Service.Get(); @@ -78,7 +78,7 @@ public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList /// /// This collection represents the list of available Aetherytes in the Teleport window. /// -public sealed partial class AetheryteList +internal sealed partial class AetheryteList { /// public int Count => this.Length; diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index dc2cb9fae..489e75bc3 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -20,7 +20,7 @@ namespace Dalamud.Game.ClientState.Buddy; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed partial class BuddyList : IServiceType, IBuddyList +internal sealed partial class BuddyList : IServiceType, IBuddyList { private const uint InvalidObjectID = 0xE0000000; @@ -147,7 +147,7 @@ public sealed partial class BuddyList : IServiceType, IBuddyList /// /// This collection represents the buddies present in your squadron or trust party. /// -public sealed partial class BuddyList +internal sealed partial class BuddyList { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 6817523af..cef802c81 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.InteropServices; using Dalamud.Data; @@ -25,7 +24,7 @@ namespace Dalamud.Game.ClientState; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class ClientState : IDisposable, IServiceType, IClientState +internal sealed class ClientState : IDisposable, IServiceType, IClientState { private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; @@ -141,7 +140,7 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState this.CfPop?.InvokeSafely(this, e); } - private void FrameworkOnOnUpdateEvent(Framework framework1) + private void FrameworkOnOnUpdateEvent(IFramework framework1) { var condition = Service.GetNullable(); var gameGui = Service.GetNullable(); diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 369e620be..305dda454 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.ClientState; /// /// Client state memory address resolver. /// -public sealed class ClientStateAddressResolver : BaseAddressResolver +internal sealed class ClientStateAddressResolver : BaseAddressResolver { // Static offsets diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 585b762bf..0f8523e9b 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -92,7 +90,7 @@ internal sealed partial class Condition : IServiceType, ICondition framework.Update += this.FrameworkUpdate; } - private void FrameworkUpdate(Framework framework) + private void FrameworkUpdate(IFramework framework) { for (var i = 0; i < MaxConditionEntries; i++) { diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 53196d5df..e9400842f 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.ClientState.Fates; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed partial class FateTable : IServiceType, IFateTable +internal sealed partial class FateTable : IServiceType, IFateTable { private readonly ClientStateAddressResolver address; @@ -110,7 +110,7 @@ public sealed partial class FateTable : IServiceType, IFateTable /// /// This collection represents the currently available Fate events. /// -public sealed partial class FateTable +internal sealed partial class FateTable { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index bc5744047..8acb6ada5 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.GamePad; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState +internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState { private readonly Hook? gamepadPoll; diff --git a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs index 683f5c61f..74e22ddbe 100644 --- a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs +++ b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.ClientState.JobGauge; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class JobGauges : IServiceType, IJobGauges +internal class JobGauges : IServiceType, IJobGauges { private Dictionary cache = new(); diff --git a/Dalamud/Game/ClientState/Keys/KeyState.cs b/Dalamud/Game/ClientState/Keys/KeyState.cs index ba5cd06d9..03c5d59b9 100644 --- a/Dalamud/Game/ClientState/Keys/KeyState.cs +++ b/Dalamud/Game/ClientState/Keys/KeyState.cs @@ -28,7 +28,7 @@ namespace Dalamud.Game.ClientState.Keys; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class KeyState : IServiceType, IKeyState +internal class KeyState : IServiceType, IKeyState { // The array is accessed in a way that this limit doesn't appear to exist // but there is other state data past this point, and keys beyond here aren't diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 16cf7c277..c6320ccbb 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.Objects; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed partial class ObjectTable : IServiceType, IObjectTable +internal sealed partial class ObjectTable : IServiceType, IObjectTable { private const int ObjectTableLength = 596; @@ -109,7 +109,7 @@ public sealed partial class ObjectTable : IServiceType, IObjectTable /// /// This collection represents the currently spawned FFXIV game objects. /// -public sealed partial class ObjectTable +internal sealed partial class ObjectTable { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index 00bcaac7d..a821ba806 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Objects; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed unsafe class TargetManager : IServiceType, ITargetManager +internal sealed unsafe class TargetManager : IServiceType, ITargetManager { [ServiceManager.ServiceDependency] private readonly ClientState clientState = Service.Get(); diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 529b57b6f..946c73245 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.ClientState.Party; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed unsafe partial class PartyList : IServiceType, IPartyList +internal sealed unsafe partial class PartyList : IServiceType, IPartyList { private const int GroupLength = 8; private const int AllianceLength = 20; @@ -130,7 +130,7 @@ public sealed unsafe partial class PartyList : IServiceType, IPartyList /// /// This collection represents the party members present in your party or alliance. /// -public sealed partial class PartyList +internal sealed partial class PartyList { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 63a1a3d09..6a8651b41 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game.Command; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class CommandManager : IServiceType, IDisposable, ICommandManager +internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager { private readonly ConcurrentDictionary commandMap = new(); private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled); diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 49d24c2a5..b77b9c4af 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -17,7 +17,7 @@ namespace Dalamud.Game.Config; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class GameConfig : IServiceType, IGameConfig, IDisposable +internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable { private readonly GameConfigAddressResolver address = new(); private Hook? configChangeHook; diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs index 6a207807a..674ee4764 100644 --- a/Dalamud/Game/Config/GameConfigAddressResolver.cs +++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs @@ -3,7 +3,7 @@ /// /// Game config system address resolver. /// -public sealed class GameConfigAddressResolver : BaseAddressResolver +internal sealed class GameConfigAddressResolver : BaseAddressResolver { /// /// Gets the address of the method called when any config option is changed. diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 2f117a492..34940dee0 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -135,7 +135,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState /// Joining a duty in progress, or disconnecting and reconnecting will cause the player to miss the event. /// /// Framework reference. - private void FrameworkOnUpdateEvent(Framework framework1) + private void FrameworkOnUpdateEvent(IFramework framework1) { // If the duty hasn't been started, and has not been completed yet this territory if (!this.IsDutyStarted && !this.CompletedThisTerritory) diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs index 436883dc2..772af79a8 100644 --- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs +++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.DutyState; /// /// Duty state memory address resolver. /// -public class DutyStateAddressResolver : BaseAddressResolver +internal class DutyStateAddressResolver : BaseAddressResolver { /// /// Gets the address of the method which is called when the client receives a content director update. diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 2b77bf400..08b97edbc 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -27,7 +27,7 @@ namespace Dalamud.Game; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class Framework : IDisposable, IServiceType, IFramework +internal sealed class Framework : IDisposable, IServiceType, IFramework { private static readonly Stopwatch StatsStopwatch = new(); @@ -39,6 +39,8 @@ public sealed class Framework : IDisposable, IServiceType, IFramework private readonly Hook updateHook; private readonly Hook destroyHook; + private readonly FrameworkAddressResolver addressResolver; + [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); @@ -54,11 +56,11 @@ public sealed class Framework : IDisposable, IServiceType, IFramework this.lifecycle = lifecycle; this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch); - this.Address = new FrameworkAddressResolver(); - this.Address.Setup(sigScanner); + this.addressResolver = new FrameworkAddressResolver(); + this.addressResolver.Setup(sigScanner); - this.updateHook = Hook.FromAddress(this.Address.TickAddress, this.HandleFrameworkUpdate); - this.destroyHook = Hook.FromAddress(this.Address.DestroyAddress, this.HandleFrameworkDestroy); + this.updateHook = Hook.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate); + this.destroyHook = Hook.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy); } /// @@ -92,9 +94,6 @@ public sealed class Framework : IDisposable, IServiceType, IFramework /// public static Dictionary> StatsHistory { get; } = new(); - /// - public FrameworkAddressResolver Address { get; } - /// public DateTime LastUpdate { get; private set; } = DateTime.MinValue; diff --git a/Dalamud/Game/FrameworkAddressResolver.cs b/Dalamud/Game/FrameworkAddressResolver.cs index 36915d7a9..c47469a01 100644 --- a/Dalamud/Game/FrameworkAddressResolver.cs +++ b/Dalamud/Game/FrameworkAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game; /// /// The address resolver for the class. /// -public sealed class FrameworkAddressResolver : BaseAddressResolver +internal sealed class FrameworkAddressResolver : BaseAddressResolver { /// /// Gets the address for the function that is called once the Framework is destroyed. diff --git a/Dalamud/Game/GameLifecycle.cs b/Dalamud/Game/GameLifecycle.cs index 5c1acc989..4192d055b 100644 --- a/Dalamud/Game/GameLifecycle.cs +++ b/Dalamud/Game/GameLifecycle.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class GameLifecycle : IServiceType, IGameLifecycle +internal class GameLifecycle : IServiceType, IGameLifecycle { private readonly CancellationTokenSource dalamudUnloadCts = new(); private readonly CancellationTokenSource gameShutdownCts = new(); diff --git a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs index 4686d5725..494e0b3ed 100644 --- a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Gui; /// /// The address resolver for the class. /// -public sealed class ChatGuiAddressResolver : BaseAddressResolver +internal sealed class ChatGuiAddressResolver : BaseAddressResolver { /// /// Gets the address of the native PrintMessage method. diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index dd1e7aa30..c126825d5 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; @@ -22,7 +21,7 @@ namespace Dalamud.Game.Gui.Dtr; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar +internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; @@ -133,7 +132,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); - private void Update(Framework unused) + private void Update(IFramework unused) { this.HandleRemovedNodes(); diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs b/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs index 588177032..677d92e57 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Gui.FlyText; /// /// An address resolver for the class. /// -public class FlyTextGuiAddressResolver : BaseAddressResolver +internal class FlyTextGuiAddressResolver : BaseAddressResolver { /// /// Gets the address of the native AddFlyText method, which occurs diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs index aa9d28cb1..c12721358 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Gui.PartyFinder; /// /// The address resolver for the class. /// -public class PartyFinderAddressResolver : BaseAddressResolver +internal class PartyFinderAddressResolver : BaseAddressResolver { /// /// Gets the address of the native ReceiveListing method. diff --git a/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs b/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs index c7630acfa..46e83b972 100644 --- a/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs +++ b/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs @@ -1,4 +1,4 @@ -using Dalamud.Data; +using Dalamud.Plugin.Services; using Lumina.Excel.GeneratedSheets; namespace Dalamud.Game.Gui.PartyFinder.Types; @@ -14,7 +14,7 @@ public static class JobFlagsExtensions /// A JobFlags enum member. /// A DataManager to get the ClassJob from. /// A ClassJob if found or null if not. - public static ClassJob ClassJob(this JobFlags job, DataManager data) + public static ClassJob? ClassJob(this JobFlags job, IDataManager data) { var jobs = data.GetExcelSheet(); @@ -52,6 +52,6 @@ public static class JobFlagsExtensions _ => null, }; - return row == null ? null : jobs.GetRow((uint)row); + return row == null ? null : jobs?.GetRow((uint)row); } } diff --git a/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs b/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs index 4f935b465..ae5426023 100644 --- a/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Gui.Toast; /// /// An address resolver for the class. /// -public class ToastGuiAddressResolver : BaseAddressResolver +internal class ToastGuiAddressResolver : BaseAddressResolver { /// /// Gets the address of the native ShowNormalToast method. diff --git a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs index ad79dff9f..a2fc08646 100644 --- a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs +++ b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics; using System.Linq; @@ -10,7 +9,7 @@ namespace Dalamud.Game.Internal.DXGI; /// The address resolver for native D3D11 methods to facilitate displaying the Dalamud UI. /// [Obsolete("This has been deprecated in favor of the VTable resolver.")] -public sealed class SwapChainSigResolver : BaseAddressResolver, ISwapChainAddressResolver +internal sealed class SwapChainSigResolver : BaseAddressResolver, ISwapChainAddressResolver { /// public IntPtr Present { get; set; } diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs index 603324175..50aae26ed 100644 --- a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs +++ b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; @@ -15,7 +14,7 @@ namespace Dalamud.Game.Internal.DXGI; /// /// If the normal signature based method of resolution fails, this is the backup. /// -public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver +internal class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver { /// public IntPtr Present { get; set; } diff --git a/Dalamud/Game/Libc/LibcFunction.cs b/Dalamud/Game/Libc/LibcFunction.cs index 7dfc26b3b..b0bd4950c 100644 --- a/Dalamud/Game/Libc/LibcFunction.cs +++ b/Dalamud/Game/Libc/LibcFunction.cs @@ -17,7 +17,7 @@ namespace Dalamud.Game.Libc; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class LibcFunction : IServiceType, ILibcFunction +internal sealed class LibcFunction : IServiceType, ILibcFunction { private readonly LibcFunctionAddressResolver address; private readonly StdStringFromCStringDelegate stdStringCtorCString; diff --git a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs index 89b721a87..4c3b7cdf8 100644 --- a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs +++ b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Libc; /// /// The address resolver for the class. /// -public sealed class LibcFunctionAddressResolver : BaseAddressResolver +internal sealed class LibcFunctionAddressResolver : BaseAddressResolver { private delegate IntPtr StringFromCString(); diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs index c698ee813..fa6af8c93 100644 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ b/Dalamud/Game/Network/GameNetworkAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Network; /// /// The address resolver for the class. /// -public sealed class GameNetworkAddressResolver : BaseAddressResolver +internal sealed class GameNetworkAddressResolver : BaseAddressResolver { /// /// Gets the address of the ProcessZonePacketDown method. diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index b5fe0b5b3..ace4654be 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -25,7 +25,7 @@ namespace Dalamud.Game; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class SigScanner : IDisposable, IServiceType, ISigScanner +internal class SigScanner : IDisposable, IServiceType, ISigScanner { private readonly FileInfo? cacheFile; diff --git a/Dalamud/Game/Text/SeStringHandling/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs index 117606a7a..ff7332f12 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs @@ -5,6 +5,7 @@ using System.IO; using Dalamud.Data; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Plugin.Services; using Newtonsoft.Json; using Serilog; @@ -27,12 +28,6 @@ public abstract partial class Payload // To force-invalidate it, Dirty can be set to true private byte[] encodedData; - /// - /// Gets the Lumina instance to use for any necessary data lookups. - /// - [JsonIgnore] - public DataManager DataResolver => Service.Get(); - /// /// Gets the type of this payload. /// @@ -43,6 +38,13 @@ public abstract partial class Payload /// public bool Dirty { get; protected set; } = true; + /// + /// Gets the Lumina instance to use for any necessary data lookups. + /// + [JsonIgnore] + // TODO: We should refactor this. It should not be possible to get IDataManager through here. + protected IDataManager DataResolver => Service.Get(); + /// /// Decodes a binary representation of a payload into its corresponding nice object payload. /// diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index b397182ef..78af0ebb7 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -442,7 +442,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP } } - private void FrameworkOnUpdate(Framework fw) + private void FrameworkOnUpdate(IFramework fw) { lock (this.activeTextures) { diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index e3cf78296..20d260704 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Services; using ImGuiNET; using ImGuiScene; @@ -358,7 +359,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable return isHover; } - private void FrameworkOnUpdate(Framework framework) + private void FrameworkOnUpdate(IFramework framework) { var clientState = Service.Get(); this.IsOpen = !clientState.IsLoggedIn; diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index a8729893f..b65f0efa7 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -6,6 +5,7 @@ using System.Reflection; using System.Threading.Tasks; using Dalamud.Game; +using Dalamud.Plugin.Services; namespace Dalamud.Logging.Internal; @@ -141,7 +141,7 @@ internal class TaskTracker : IDisposable, IServiceType return true; } - private void FrameworkOnUpdate(Framework framework) + private void FrameworkOnUpdate(IFramework framework) { UpdateData(); } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs index 8ea55856c..7001e4d7b 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -7,6 +6,7 @@ using CheapLoc; using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Game.Gui; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -78,7 +78,7 @@ internal class ProfileCommandHandler : IServiceType, IDisposable this.framework.Update += this.FrameworkOnUpdate; } - private void FrameworkOnUpdate(Framework framework1) + private void FrameworkOnUpdate(IFramework framework1) { if (this.profileManager.IsBusy) return; diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs index 69c21bca4..334577b92 100644 --- a/Dalamud/Plugin/Services/IFramework.cs +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -15,18 +15,13 @@ public interface IFramework /// A delegate type used with the event. /// /// The Framework instance. - public delegate void OnUpdateDelegate(Framework framework); + public delegate void OnUpdateDelegate(IFramework framework); /// /// Event that gets fired every time the game framework updates. /// public event OnUpdateDelegate Update; - /// - /// Gets a raw pointer to the instance of Client::Framework. - /// - public FrameworkAddressResolver Address { get; } - /// /// Gets the last time that the Framework Update event was triggered. /// From 00fa1dc4f83beea0373aa15ddcaef16a9bf88547 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 23:02:42 +0200 Subject: [PATCH 106/122] chore: remove ChatHandlers public service --- Dalamud/Game/ChatHandlers.cs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 1d82e5f9c..896d296fc 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -25,10 +25,8 @@ namespace Dalamud.Game; /// /// Chat events and public helper functions. /// -[PluginInterface] -[InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public class ChatHandlers : IServiceType +internal class ChatHandlers : IServiceType { // private static readonly Dictionary UnicodeToDiscordEmojiDict = new() // { @@ -134,22 +132,6 @@ public class ChatHandlers : IServiceType /// public bool IsAutoUpdateComplete { get; private set; } - /// - /// Convert a TextPayload to SeString and wrap in italics payloads. - /// - /// Text to convert. - /// SeString payload of italicized text. - public static SeString MakeItalics(string text) - => MakeItalics(new TextPayload(text)); - - /// - /// Convert a TextPayload to SeString and wrap in italics payloads. - /// - /// Text to convert. - /// SeString payload of italicized text. - public static SeString MakeItalics(TextPayload text) - => new(EmphasisItalicPayload.ItalicsOn, text, EmphasisItalicPayload.ItalicsOff); - private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) { var textVal = message.TextValue; From 3d94d07f56b0d959b10ad1f5ab9c025e1ade8c69 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 23:13:16 +0200 Subject: [PATCH 107/122] chore: remove opcodes from public API --- Dalamud/Plugin/Services/IDataManager.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index 3ae10b0c7..4977b65b3 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -19,17 +19,7 @@ public interface IDataManager /// Gets the current game client language. /// public ClientLanguage Language { get; } - - /// - /// Gets the OpCodes sent by the server to the client. - /// - public ReadOnlyDictionary ServerOpCodes { get; } - - /// - /// Gets the OpCodes sent by the client to the server. - /// - public ReadOnlyDictionary ClientOpCodes { get; } - + /// /// Gets a object which gives access to any excel/game data. /// From 9d4a2fad3bd1fff50cd1bfd6d7f44fdb13df0695 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 23:39:09 +0200 Subject: [PATCH 108/122] fix dtr errors after merge --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 3 ++- Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 4e9584f27..42f06443f 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; diff --git a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs index 1e6fd09cd..744d926f0 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs @@ -3,7 +3,7 @@ /// /// DtrBar memory address resolver. /// -public class DtrBarAddressResolver : BaseAddressResolver +internal class DtrBarAddressResolver : BaseAddressResolver { /// /// Gets the address of the AtkUnitBaseDraw method. From 4989e2b69b8ce23dbe01b8a6786267e6a0ed6ea2 Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Mon, 18 Sep 2023 03:55:43 +0200 Subject: [PATCH 109/122] refactor: rename fly text kind members --- Dalamud/Game/Gui/FlyText/FlyTextKind.cs | 144 +++++++++++++----------- 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs index 68650fb5c..3727fd0f8 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs @@ -1,57 +1,58 @@ namespace Dalamud.Game.Gui.FlyText; /// -/// Enum of FlyTextKind values. Members suffixed with -/// a number seem to be a duplicate, or perform duplicate behavior. +/// Enum of FlyTextKind values. /// public enum FlyTextKind : int { /// /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// Used for autos and incoming DoTs. /// - AutoAttack = 0, + AutoAttackOrDot = 0, /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// Does a bounce effect on appearance. /// - DirectHit = 1, + AutoAttackOrDotDh = 1, /// /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. /// Does a bigger bounce effect on appearance. /// - CriticalHit = 2, + AutoAttackOrDotCrit = 2, /// - /// Val1 in even larger serif font with 2 exclamations, Text2 in - /// sans-serif as subtitle. Does a large bounce effect on appearance. - /// Does not scroll up or down the screen. + /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle. + /// Does a large bounce effect on appearance. Does not scroll up or down the screen. /// - CriticalDirectHit = 3, + AutoAttackOrDotCritDh = 3, /// - /// AutoAttack with sans-serif Text1 to the left of the Val1. + /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. /// - NamedAttack = 4, + Damage = 4, /// - /// DirectHit with sans-serif Text1 to the left of the Val1. + /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. + /// Does a bounce effect on appearance. /// - NamedDirectHit = 5, + DamageDh = 5, /// - /// CriticalHit with sans-serif Text1 to the left of the Val1. + /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. + /// Does a bigger bounce effect on appearance. /// - NamedCriticalHit = 6, + DamageCrit = 6, /// - /// CriticalDirectHit with sans-serif Text1 to the left of the Val1. + /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. + /// Does a large bounce effect on appearance. Does not scroll up or down the screen. /// - NamedCriticalDirectHit = 7, + DamageCritDh = 7, /// + /// The text changes to DODGE under certain circumstances. /// All caps, serif MISS. /// Miss = 8, @@ -74,12 +75,12 @@ public enum FlyTextKind : int /// /// Icon next to sans-serif Text1. /// - NamedIcon = 12, + Buff = 12, /// - /// Icon next to sans-serif Text1 (2). + /// Icon next to sans-serif Text1. /// - NamedIcon2 = 13, + Debuff = 13, /// /// Serif Val1 with all caps condensed font EXP with Text2 in sans-serif as subtitle. @@ -94,42 +95,44 @@ public enum FlyTextKind : int /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// - NamedMp = 16, + MpDrain = 16, /// + /// Currently not used by the game. /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// NamedTp = 17, /// - /// AutoAttack with sans-serif Text1 to the left of the Val1 (2). + /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. /// - NamedAttack2 = 18, + Healing = 18, /// - /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (2). + /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// - NamedMp2 = 19, + MpRegen = 19, /// - /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (2). + /// Currently not used by the game. + /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// NamedTp2 = 20, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font EP with Text2 in sans-serif as subtitle. /// - NamedEp = 21, + EpRegen = 21, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font CP with Text2 in sans-serif as subtitle. /// - NamedCp = 22, + CpRegen = 22, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font GP with Text2 in sans-serif as subtitle. /// - NamedGp = 23, + GpRegen = 23, /// /// Displays nothing. @@ -149,57 +152,59 @@ public enum FlyTextKind : int Interrupted = 26, /// - /// AutoAttack with no Text2. + /// Val1 in serif font. /// - AutoAttackNoText = 27, + CraftingProgress = 27, /// - /// AutoAttack with no Text2 (2). + /// Val1 in serif font. /// - AutoAttackNoText2 = 28, + CraftingQuality = 28, /// - /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance (2). + /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance. /// - CriticalHit2 = 29, + CraftingQualityCrit = 29, /// - /// AutoAttack with no Text2 (3). + /// Currently not used by the game. + /// Val1 in serif font. /// AutoAttackNoText3 = 30, /// /// CriticalHit with sans-serif Text1 to the left of the Val1 (2). /// - NamedCriticalHit2 = 31, + HealingCrit = 31, /// - /// Same as NamedCriticalHit with a green (cannot change) MP in condensed font to the right of Val1. + /// Currently not used by the game. + /// Same as DamageCrit with a MP in condensed font to the right of Val1. /// Does a jiggle effect to the right on appearance. /// NamedCriticalHitWithMp = 32, /// - /// Same as NamedCriticalHit with a yellow (cannot change) TP in condensed font to the right of Val1. + /// Currently not used by the game. + /// Same as DamageCrit with a TP in condensed font to the right of Val1. /// Does a jiggle effect to the right on appearance. /// NamedCriticalHitWithTp = 33, /// - /// Same as NamedIcon with sans-serif "has no effect!" to the right. + /// Icon next to sans-serif Text1 with sans-serif "has no effect!" to the right. /// - NamedIconHasNoEffect = 34, + DebuffNoEffect = 34, /// - /// Same as NamedIcon but Text1 is slightly faded. Used for buff expiration. + /// Icon next to sans-serif slightly faded Text1. /// - NamedIconFaded = 35, + BuffFading = 35, /// - /// Same as NamedIcon but Text1 is slightly faded (2). - /// Used for buff expiration. + /// Icon next to sans-serif slightly faded Text1. /// - NamedIconFaded2 = 36, + DebuffFading = 36, /// /// Text1 in sans-serif font. @@ -207,9 +212,9 @@ public enum FlyTextKind : int Named = 37, /// - /// Same as NamedIcon with sans-serif "(fully resisted)" to the right. + /// Icon next to sans-serif Text1 with sans-serif "(fully resisted)" to the right. /// - NamedIconFullyResisted = 38, + DebuffResisted = 38, /// /// All caps serif 'INCAPACITATED!'. @@ -219,32 +224,34 @@ public enum FlyTextKind : int /// /// Text1 with sans-serif "(fully resisted)" to the right. /// - NamedFullyResisted = 40, + FullyResisted = 40, /// /// Text1 with sans-serif "has no effect!" to the right. /// - NamedHasNoEffect = 41, + HasNoEffect = 41, /// - /// AutoAttack with sans-serif Text1 to the left of the Val1 (3). + /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. /// - NamedAttack3 = 42, + HpDrain = 42, /// - /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (3). + /// Currently not used by the game. + /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// NamedMp3 = 43, /// - /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (3). + /// Currently not used by the game. + /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// NamedTp3 = 44, /// - /// Same as NamedIcon with serif "INVULNERABLE!" beneath the Text1. + /// Icon next to sans-serif Text1 with serif "INVULNERABLE!" beneath the Text1. /// - NamedIconInvulnerable = 45, + DebuffInvulnerable = 45, /// /// All caps serif RESIST. @@ -252,20 +259,20 @@ public enum FlyTextKind : int Resist = 46, /// - /// Same as NamedIcon but places the given icon in the item icon outline. + /// Icon with an item icon outline next to sans-serif Text1. /// - NamedIconWithItemOutline = 47, + LootedItem = 47, /// - /// AutoAttack with no Text2 (4). + /// Val1 in serif font. /// - AutoAttackNoText4 = 48, + Collectability = 48, /// - /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (3). + /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. /// Does a bigger bounce effect on appearance. /// - CriticalHit3 = 49, + CollectabilityCrit = 49, /// /// All caps serif REFLECT. @@ -278,20 +285,21 @@ public enum FlyTextKind : int Reflected = 51, /// - /// Val1 in serif font, Text2 in sans-serif as subtitle (2). + /// Val1 in serif font, Text2 in sans-serif as subtitle. /// Does a bounce effect on appearance. /// - DirectHit2 = 52, + CraftingQualityDh = 52, /// - /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (4). + /// Currently not used by the game. + /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. /// Does a bigger bounce effect on appearance. /// CriticalHit4 = 53, /// - /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle (2). + /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle. /// Does a large bounce effect on appearance. Does not scroll up or down the screen. /// - CriticalDirectHit2 = 54, + CraftingQualityCritDh = 54, } From 5a3196e5f89d6675ee91ecbf00e95f7336dd40be Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:21:01 -0700 Subject: [PATCH 110/122] [AddonLifecycle] Fix incorrect delegate signature (#1401) --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 5fc1c7d2b..c416b6d1f 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -63,7 +63,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate void AddonOnRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); - private delegate void AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); + private delegate byte AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); /// public void Dispose() @@ -221,7 +221,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - private void OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) + private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) { try { @@ -232,7 +232,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke."); } - this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values); + var result = this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values); try { @@ -242,6 +242,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); } + + return result; } private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) From af39add38e3f93761f0774fd488245613af0c331 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 18 Sep 2023 09:18:01 +0200 Subject: [PATCH 111/122] [v9] Update ClientStructs and fix build (#1398) * Update ClientStructs * Use IFramework in AddonLifecycle * Fix for ClientStructs breaking changes --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 4 ++-- Dalamud/Interface/Internal/UiDebug.cs | 10 +++++----- lib/FFXIVClientStructs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 5fc1c7d2b..5a1070dfb 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -97,7 +97,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } // Used to prevent concurrency issues if plugins try to register during iteration of listeners. - private void OnFrameworkUpdate(Framework unused) + private void OnFrameworkUpdate(IFramework unused) { if (this.newEventListeners.Any()) { diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index b1f27828c..524759f4a 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -1,5 +1,6 @@ using System; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.Game; @@ -416,7 +417,7 @@ internal unsafe class UiDebug $"MultiplyRGB: {node->MultiplyRed} {node->MultiplyGreen} {node->MultiplyBlue}"); } - private bool DrawUnitListHeader(int index, uint count, ulong ptr, bool highlight) + private bool DrawUnitListHeader(int index, ushort count, ulong ptr, bool highlight) { ImGui.PushStyleColor(ImGuiCol.Text, highlight ? 0xFFAAAA00 : 0xFFFFFFFF); if (!string.IsNullOrEmpty(this.searchInput) && !this.doingSearch) @@ -455,8 +456,6 @@ internal unsafe class UiDebug this.selectedInList[i] = false; var unitManager = &unitManagers[i]; - var unitBaseArray = &unitManager->AtkUnitEntries; - var headerOpen = true; if (!searching) @@ -468,7 +467,7 @@ internal unsafe class UiDebug for (var j = 0; j < unitManager->Count && headerOpen; j++) { - var unitBase = unitBaseArray[j]; + var unitBase = *(AtkUnitBase**)Unsafe.AsPointer(ref unitManager->EntriesSpan[j]); if (this.selectedUnitBase != null && unitBase == this.selectedUnitBase) { this.selectedInList[i] = true; @@ -513,7 +512,8 @@ internal unsafe class UiDebug { for (var j = 0; j < unitManager->Count; j++) { - if (this.selectedUnitBase == null || unitBaseArray[j] != this.selectedUnitBase) continue; + var unitBase = *(AtkUnitBase**)Unsafe.AsPointer(ref unitManager->EntriesSpan[j]); + if (this.selectedUnitBase == null || unitBase != this.selectedUnitBase) continue; this.selectedInList[i] = true; foundSelected = true; } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 06e3ca233..fd5ba8a27 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 06e3ca2336031ba86ef95d022a2af722e5d00a7e +Subproject commit fd5ba8a27ec911a69eeb93ceb0202091279dfceb From 7a0f58a1063adef5223ca5e8a364cffdfd45d19e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 00:19:31 -0700 Subject: [PATCH 112/122] Remove PluginInterface from base Dtr service (#1399) --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 42f06443f..5467e207f 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -22,12 +22,8 @@ namespace Dalamud.Game.Gui.Dtr; /// /// Class used to interface with the server info bar. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; From 3e8be33a5cfb916bf28633521fe228b6e570a609 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:45:42 -0700 Subject: [PATCH 113/122] Add CommandManagerPluginScoped --- Dalamud/Game/Command/CommandManager.cs | 76 ++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 6a8651b41..56630a5ad 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -17,12 +16,8 @@ namespace Dalamud.Game.Command; /// /// This class manages registered in-game slash commands. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager { private readonly ConcurrentDictionary commandMap = new(); @@ -84,7 +79,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage // => command: 0-12 (12 chars) // => argument: 13-17 (4 chars) // => content.IndexOf(' ') == 12 - command = content.Substring(0, separatorPosition); + command = content[..separatorPosition]; var argStart = separatorPosition + 1; argument = content[argStart..]; @@ -162,3 +157,72 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage } } } + +/// +/// Plugin-scoped version of a AddonLifecycle service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandManager +{ + [ServiceManager.ServiceDependency] + private readonly CommandManager commandManagerService = Service.Get(); + + private readonly List pluginRegisteredCommands = new(); + + /// + public ReadOnlyDictionary Commands => this.commandManagerService.Commands; + + /// + public void Dispose() + { + foreach (var command in this.pluginRegisteredCommands) + { + this.commandManagerService.RemoveHandler(command); + } + + this.pluginRegisteredCommands.Clear(); + } + + /// + public bool ProcessCommand(string content) + => this.commandManagerService.ProcessCommand(content); + + /// + public void DispatchCommand(string command, string argument, CommandInfo info) + => this.commandManagerService.DispatchCommand(command, argument, info); + + /// + public bool AddHandler(string command, CommandInfo info) + { + if (!this.pluginRegisteredCommands.Contains(command)) + { + if (this.commandManagerService.AddHandler(command, info)) + { + this.pluginRegisteredCommands.Add(command); + return true; + } + } + + return false; + } + + /// + public bool RemoveHandler(string command) + { + if (this.pluginRegisteredCommands.Contains(command)) + { + if (this.commandManagerService.RemoveHandler(command)) + { + this.pluginRegisteredCommands.Remove(command); + return true; + } + } + + return false; + } +} From 34617cf377b7805b0fa60296fcfccb3af73c6ca1 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:50:05 -0700 Subject: [PATCH 114/122] Add error logging --- Dalamud/Game/Command/CommandManager.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 56630a5ad..118d210e2 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -8,8 +8,8 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using Serilog; namespace Dalamud.Game.Command; @@ -20,6 +20,8 @@ namespace Dalamud.Game.Command; [ServiceManager.BlockingEarlyLoadedService] internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager { + private static readonly ModuleLog Log = new("Command"); + private readonly ConcurrentDictionary commandMap = new(); private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled); private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?.+)$", RegexOptions.Compiled); @@ -169,6 +171,8 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage #pragma warning restore SA1015 internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandManager { + private static readonly ModuleLog Log = new("Command"); + [ServiceManager.ServiceDependency] private readonly CommandManager commandManagerService = Service.Get(); @@ -207,6 +211,10 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM return true; } } + else + { + Log.Error($"Command {command} is already registered."); + } return false; } @@ -222,6 +230,10 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM return true; } } + else + { + Log.Error($"Command {command} not found."); + } return false; } From 5d0694918572281ba0ac2a00915691406bf5f2ef Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:00:49 -0700 Subject: [PATCH 115/122] Set CommandInfo InternalName when adding to CommandManager via PluginScopedService --- Dalamud/Game/Command/CommandInfo.cs | 1 - Dalamud/Game/Command/CommandManager.cs | 12 ++++++++++++ .../Windows/PluginInstaller/PluginInstallerWindow.cs | 4 +--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Command/CommandInfo.cs b/Dalamud/Game/Command/CommandInfo.cs index 9b559599a..bc0250a66 100644 --- a/Dalamud/Game/Command/CommandInfo.cs +++ b/Dalamud/Game/Command/CommandInfo.cs @@ -15,7 +15,6 @@ public sealed class CommandInfo public CommandInfo(HandlerDelegate handler) { this.Handler = handler; - this.LoaderAssemblyName = Assembly.GetCallingAssembly()?.GetName()?.Name; } /// diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 118d210e2..218b89676 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -9,6 +9,7 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; namespace Dalamud.Game.Command; @@ -177,7 +178,17 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM private readonly CommandManager commandManagerService = Service.Get(); private readonly List pluginRegisteredCommands = new(); + private readonly LocalPlugin pluginInfo; + /// + /// Initializes a new instance of the class. + /// + /// Info for the plugin that requests this service. + public CommandManagerPluginScoped(LocalPlugin localPlugin) + { + this.pluginInfo = localPlugin; + } + /// public ReadOnlyDictionary Commands => this.commandManagerService.Commands; @@ -205,6 +216,7 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM { if (!this.pluginRegisteredCommands.Contains(command)) { + info.LoaderAssemblyName = this.pluginInfo.InternalName; if (this.commandManagerService.AddHandler(command, info)) { this.pluginRegisteredCommands.Add(command); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index e5d3c147b..dcbdced28 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -15,7 +15,6 @@ using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -2227,8 +2226,7 @@ internal class PluginInstallerWindow : Window, IDisposable { var commands = commandManager.Commands .Where(cInfo => - cInfo.Value != null && - cInfo.Value.ShowInHelp && + cInfo.Value is { ShowInHelp: true } && cInfo.Value.LoaderAssemblyName == plugin.Manifest.InternalName) .ToArray(); From dde1dd2f552fdfc85ac3940643706749645700d9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:16:45 -0700 Subject: [PATCH 116/122] Add Clear button to NetworkMonitorWidget --- .../Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 8212c2e95..e7bce0b84 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -98,6 +98,11 @@ internal class NetworkMonitorWidget : IDataWindowWidget this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512); } + if (ImGui.Button("Clear Stored Packets")) + { + this.packets.Clear(); + } + this.DrawFilterInput(); this.DrawNegativeFilterInput(); From 84c5982c258c4bf714278aa4cf6cdac3182e5676 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 19 Sep 2023 15:17:15 +0900 Subject: [PATCH 117/122] Use axis12 glyph ranges when loading noto12 as Dalamud default font --- .../Interface/GameFonts/GameFontManager.cs | 36 +++++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 29 +++++++-------- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index ad0e47273..3a1ab737e 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -171,6 +171,42 @@ internal class GameFontManager : IServiceType fontPtr.BuildLookupTable(); } + /// + /// Create a glyph range for use with ImGui AddFont. + /// + /// Font family and size. + /// Merge two ranges into one if distance is below the value specified in this parameter. + /// Glyph ranges. + public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8) + { + var fdt = this.fdts[(int)family]!; + var ranges = new List(fdt.Glyphs.Count) + { + checked((ushort)fdt.Glyphs[0].CharInt), + checked((ushort)fdt.Glyphs[0].CharInt), + }; + + foreach (var glyph in fdt.Glyphs.Skip(1)) + { + var c32 = glyph.CharInt; + if (c32 >= 0x10000) + break; + + var c16 = unchecked((ushort)c32); + if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) + { + ranges[^1] = c16; + } + else if (ranges[^1] + 1 < c16) + { + ranges.Add(c16); + ranges.Add(c16); + } + } + + return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned); + } + /// /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 841511f55..559207ed6 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -790,10 +790,10 @@ internal class InterfaceManager : IDisposable, IServiceType } else { - var japaneseRangeHandle = GCHandle.Alloc(GlyphRangesJapanese.GlyphRanges, GCHandleType.Pinned); - garbageList.Add(japaneseRangeHandle); + var rangeHandle = gameFontManager.ToGlyphRanges(GameFontFamilyAndSize.Axis12); + garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = japaneseRangeHandle.AddrOfPinnedObject(); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); fontConfig.PixelSnapH = true; DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); this.loadedFontInfo[DefaultFont] = fontInfo; @@ -850,22 +850,19 @@ internal class InterfaceManager : IDisposable, IServiceType foreach (var (fontSize, requests) in extraFontRequests) { - List> codepointRanges = new(); - codepointRanges.Add(Tuple.Create(Fallback1Codepoint, Fallback1Codepoint)); - codepointRanges.Add(Tuple.Create(Fallback2Codepoint, Fallback2Codepoint)); - - // ImGui default ellipsis characters - codepointRanges.Add(Tuple.Create(0x2026, 0x2026)); - codepointRanges.Add(Tuple.Create(0x0085, 0x0085)); + List<(ushort, ushort)> codepointRanges = new(4 + requests.Sum(x => x.CodepointRanges.Count)) + { + new(Fallback1Codepoint, Fallback1Codepoint), + new(Fallback2Codepoint, Fallback2Codepoint), + // ImGui default ellipsis characters + new(0x2026, 0x2026), + new(0x0085, 0x0085), + }; foreach (var request in requests) - { - foreach (var range in request.CodepointRanges) - codepointRanges.Add(range); - } - - codepointRanges.Sort((x, y) => (x.Item1 == y.Item1 ? (x.Item2 < y.Item2 ? -1 : (x.Item2 == y.Item2 ? 0 : 1)) : (x.Item1 < y.Item1 ? -1 : 1))); + codepointRanges.AddRange(request.CodepointRanges.Select(x => (From: x.Item1, To: x.Item2))); + codepointRanges.Sort(); List flattenedRanges = new(); foreach (var range in codepointRanges) { From a61e181bde092988c5fac6c984b2dead307179db Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 19 Sep 2023 19:28:26 +0200 Subject: [PATCH 118/122] fix warning in DataManager --- Dalamud/Data/DataManager.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 809726684..f1f98229a 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -126,10 +126,14 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager /// public ClientLanguage Language { get; private set; } - /// + /// + /// Gets a list of server opcodes. + /// public ReadOnlyDictionary ServerOpCodes { get; private set; } - - /// + + /// + /// Gets a list of client opcodes. + /// [UsedImplicitly] public ReadOnlyDictionary ClientOpCodes { get; private set; } From f7ae34281f530343421b40aac61270ee997f2ada Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 19 Sep 2023 14:11:23 -0700 Subject: [PATCH 119/122] Update License to AGPL 3.0 or Later (#1373) --- Dalamud/Dalamud.csproj | 1 + LICENSE | 671 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 625 insertions(+), 47 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index af785cf52..bcc06d435 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -13,6 +13,7 @@ $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) + AGPL-3.0-or-later diff --git a/LICENSE b/LICENSE index 946b95d8d..0ad25db4b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,84 +1,661 @@ -AFFERO GENERAL PUBLIC LICENSE -Version 1, March 2002

Copyright © 2002 Affero Inc.
510 Third Street - Suite 225, San Francisco, CA 94107, USA -This license is a modified version of the GNU General Public License copyright (C) 1989, 1991 Free Software Foundation, Inc. made with their permission. Section 2(d) has been added to cover use of software over a computer network. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Preamble + Preamble -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the Affero General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This Public License applies to most of Affero's software and to any other program whose authors commit to using it. (Some other Affero software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. -When we speak of free software, we are referring to freedom, not price. This General Public License is designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. -To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. -For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. -We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. -Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. -Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. -The precise terms and conditions for copying, distribution and modification follow. + The precise terms and conditions for copying, distribution and +modification follow. -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + TERMS AND CONDITIONS -0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this Affero General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + 0. Definitions. -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + "This License" refers to version 3 of the GNU Affero General Public License. -1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. -2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
 -a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
 -b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
 -c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
 -d) If the Program as you received it is intended to interact with users through a computer network and if, in the version you received, any user interacting with the Program was given the opportunity to request transmission to that user of the Program's complete source code, you must not remove that facility from your modified version of the Program or work based on the Program, and must offer an equivalent opportunity for all users interacting with your Program through a computer network to request immediate transmission by HTTP of the complete source code of your modified version or other derivative work. + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + A "covered work" means either the unmodified Program or a work based +on the Program. -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. -In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. -3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
 -a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
 -b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
 -c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. -The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + 1. Source Code. -If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. -4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. -5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. -6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. -7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + The Corresponding Source for a work in source code form is that +same work. -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + 2. Basic Permissions. -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. -8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. -9. Affero Inc. may publish revised and/or new versions of the Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. -Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by Affero, Inc. If the Program does not specify a version number of this License, you may choose any version ever published by Affero, Inc. + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. -You may also choose to redistribute modified versions of this program under any version of the Free Software Foundation's GNU General Public License version 3 or higher, so long as that version of the GNU GPL includes terms and conditions substantially equivalent to those of this license. + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. -10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by Affero, Inc., write to us; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. -NO WARRANTY + 4. Conveying Verbatim Copies. -11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. -12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. From 979a5463ca14d60f558c61f06872a067c126fba9 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 19 Sep 2023 23:14:50 +0200 Subject: [PATCH 120/122] fix: dev plugins always need to retain their WorkingPluginId, even throughout reloads --- .../Internal/DevPluginSettings.cs | 7 ++++++ .../Plugin/Internal/Types/LocalDevPlugin.cs | 22 ++++++++++++++++++ Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 23 ++++++++++++++----- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs index 939b03eca..cfe8ba411 100644 --- a/Dalamud/Configuration/Internal/DevPluginSettings.cs +++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs @@ -1,3 +1,5 @@ +using System; + namespace Dalamud.Configuration.Internal; /// @@ -14,4 +16,9 @@ internal sealed class DevPluginSettings /// Gets or sets a value indicating whether this plugin should automatically reload on file change. /// public bool AutomaticReloading { get; set; } = false; + + /// + /// Gets or sets an ID uniquely identifying this specific instance of a devPlugin. + /// + public Guid WorkingPluginId { get; set; } = Guid.Empty; } diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 98784ce64..580d5c161 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -40,6 +41,22 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings(); configuration.QueueSave(); } + + // Legacy dev plugins might not have this! + if (this.devSettings.WorkingPluginId == Guid.Empty) + { + this.devSettings.WorkingPluginId = Guid.NewGuid(); + Log.Verbose("{InternalName} was assigned new devPlugin GUID {Guid}", this.InternalName, this.devSettings.WorkingPluginId); + configuration.QueueSave(); + } + + // If the ID in the manifest is wrong, force the good one + if (this.DevImposedWorkingPluginId != this.manifest.WorkingPluginId) + { + Debug.Assert(this.DevImposedWorkingPluginId != Guid.Empty, "Empty guid for devPlugin"); + this.manifest.WorkingPluginId = this.DevImposedWorkingPluginId; + this.SaveManifest("dev imposed working plugin id"); + } if (this.AutomaticReload) { @@ -76,6 +93,11 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable } } } + + /// + /// Gets an ID uniquely identifying this specific instance of a devPlugin. + /// + public Guid DevImposedWorkingPluginId => this.devSettings.WorkingPluginId; /// public new void Dispose() diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 115ab0f8d..f7306b5a7 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -26,6 +26,13 @@ namespace Dalamud.Plugin.Internal.Types; /// internal class LocalPlugin : IDisposable { + /// + /// The underlying manifest for this plugin. + /// +#pragma warning disable SA1401 + protected LocalPluginManifest manifest; +#pragma warning restore SA1401 + private static readonly ModuleLog Log = new("LOCALPLUGIN"); private readonly FileInfo manifestFile; @@ -39,8 +46,6 @@ internal class LocalPlugin : IDisposable private Type? pluginType; private IDalamudPlugin? instance; - private LocalPluginManifest manifest; - /// /// Initializes a new instance of the class. /// @@ -659,9 +664,11 @@ internal class LocalPlugin : IDisposable var manifestPath = LocalPluginManifest.GetManifestFile(this.DllFile); if (manifestPath.Exists) { - // var isDisabled = this.IsDisabled; // saving the internal state because it could have been deleted + // Save some state that we do actually want to carry over + var guid = this.manifest.WorkingPluginId; + this.manifest = LocalPluginManifest.Load(manifestPath) ?? throw new Exception("Could not reload manifest."); - // this.manifest.Disabled = isDisabled; + this.manifest.WorkingPluginId = guid; this.SaveManifest("dev reload"); } @@ -686,6 +693,12 @@ internal class LocalPlugin : IDisposable }); } + /// + /// Save this plugin manifest. + /// + /// Why it should be saved. + protected void SaveManifest(string reason) => this.manifest.Save(this.manifestFile, reason); + private static void SetupLoaderConfig(LoaderConfig config) { config.IsUnloadable = true; @@ -694,6 +707,4 @@ internal class LocalPlugin : IDisposable config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName()); config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); } - - private void SaveManifest(string reason) => this.manifest.Save(this.manifestFile, reason); } From c305c01dfdc8482a6c5abf4fdf5b290f55d57529 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:43:45 -0700 Subject: [PATCH 121/122] Nullify Scoped Service Delegates --- Dalamud/Game/ClientState/Conditions/Condition.cs | 2 ++ Dalamud/Game/DutyState/DutyState.cs | 5 +++++ Dalamud/Game/Gui/ChatGui.cs | 5 +++++ Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 2 ++ Dalamud/Game/Gui/GameGui.cs | 4 ++++ Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs | 2 ++ Dalamud/Game/Gui/Toast/ToastGui.cs | 4 ++++ Dalamud/Game/Network/GameNetwork.cs | 2 ++ Dalamud/Utility/Util.cs | 2 +- 9 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 0f8523e9b..2db47ea4d 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -189,6 +189,8 @@ internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition public void Dispose() { this.conditionService.ConditionChange -= this.ConditionChangedForward; + + this.ConditionChange = null; } /// diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 34940dee0..c52ceff0f 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -210,6 +210,11 @@ internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState this.dutyStateService.DutyWiped -= this.DutyWipedForward; this.dutyStateService.DutyRecommenced -= this.DutyRecommencedForward; this.dutyStateService.DutyCompleted -= this.DutyCompletedForward; + + this.DutyStarted = null; + this.DutyWiped = null; + this.DutyRecommenced = null; + this.DutyCompleted = null; } private void DutyStartedForward(object sender, ushort territoryId) => this.DutyStarted?.Invoke(sender, territoryId); diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 2fbeb404e..55c919ab5 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -460,6 +460,11 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward; this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward; this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward; + + this.ChatMessage = null; + this.CheckMessageHandled = null; + this.ChatMessageHandled = null; + this.ChatMessageUnhandled = null; } /// diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 3c04c744a..64de4b2dd 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -307,6 +307,8 @@ internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui public void Dispose() { this.flyTextGuiService.FlyTextCreated -= this.FlyTextCreatedForward; + + this.FlyTextCreated = null; } /// diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 078c624e8..349d2a424 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -566,6 +566,10 @@ internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; this.gameGuiService.HoveredActionChanged -= this.HoveredActionForward; + + this.UiHideToggled = null; + this.HoveredItemChanged = null; + this.HoveredActionChanged = null; } /// diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 41a8ba56a..4bd93cdf0 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -156,6 +156,8 @@ internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFin public void Dispose() { this.partyFinderGuiService.ReceiveListing -= this.ReceiveListingForward; + + this.ReceiveListing = null; } private void ReceiveListingForward(PartyFinderListing listing, PartyFinderListingEventArgs args) => this.ReceiveListing?.Invoke(listing, args); diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 93126710b..9624e3e72 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -417,6 +417,10 @@ internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui this.toastGuiService.Toast -= this.ToastForward; this.toastGuiService.QuestToast -= this.QuestToastForward; this.toastGuiService.ErrorToast -= this.ErrorToastForward; + + this.Toast = null; + this.QuestToast = null; + this.ErrorToast = null; } /// diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index f56fd3996..7c900ece4 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -169,6 +169,8 @@ internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork public void Dispose() { this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward; + + this.NetworkMessage = null; } private void NetworkMessageForward(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 78edda3dd..c386ee23e 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -651,7 +651,7 @@ public static class Util /// /// The path of the file to write to. /// The text to write. - internal static void WriteAllTextSafe(string path, string text) + public static void WriteAllTextSafe(string path, string text) { var tmpPath = path + ".tmp"; if (File.Exists(tmpPath)) From 2b2a027fb08aca6e7400b2cf5afbb886a8f9ac76 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:44:56 -0700 Subject: [PATCH 122/122] Move WriteAllTextSafe to correct location --- Dalamud/Utility/Util.cs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index c386ee23e..8ca87b691 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -609,7 +609,23 @@ public static class Util } } } + + /// + /// Overwrite text in a file by first writing it to a temporary file, and then + /// moving that file to the path specified. + /// + /// The path of the file to write to. + /// The text to write. + public static void WriteAllTextSafe(string path, string text) + { + var tmpPath = path + ".tmp"; + if (File.Exists(tmpPath)) + File.Delete(tmpPath); + File.WriteAllText(tmpPath, text); + File.Move(tmpPath, path, true); + } + /// /// Dispose this object. /// @@ -645,22 +661,6 @@ public static class Util } } - /// - /// Overwrite text in a file by first writing it to a temporary file, and then - /// moving that file to the path specified. - /// - /// The path of the file to write to. - /// The text to write. - public static void WriteAllTextSafe(string path, string text) - { - var tmpPath = path + ".tmp"; - if (File.Exists(tmpPath)) - File.Delete(tmpPath); - - File.WriteAllText(tmpPath, text); - File.Move(tmpPath, path, true); - } - /// /// Gets a random, inoffensive, human-friendly string. ///