From ddf0a97c83f5d585ec54e82f43f974442a203f5a Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 1 May 2025 20:47:03 +0200 Subject: [PATCH] Add plugin error notifications, per-plugin event invocation wrappers --- .../Internal/DevPluginSettings.cs | 10 +- Dalamud/Console/ConsoleManagerPluginScoped.cs | 82 ++++---- Dalamud/Game/Framework.cs | 11 +- .../Interface/Internal/DalamudInterface.cs | 8 +- .../Internal/Windows/ConsoleWindow.cs | 76 ++++--- .../PluginInstaller/PluginInstallerWindow.cs | 20 ++ Dalamud/Logging/ScopedPluginLogService.cs | 23 +- Dalamud/Plugin/Internal/PluginErrorHandler.cs | 198 ++++++++++++++++++ .../Plugin/Internal/Types/LocalDevPlugin.cs | 10 + Dalamud/Service/ServiceManager.cs | 2 +- Dalamud/Service/Service{T}.cs | 3 + 11 files changed, 358 insertions(+), 85 deletions(-) create mode 100644 Dalamud/Plugin/Internal/PluginErrorHandler.cs diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs index 361632a14..64327e658 100644 --- a/Dalamud/Configuration/Internal/DevPluginSettings.cs +++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs @@ -12,16 +12,22 @@ internal sealed class DevPluginSettings /// public bool StartOnBoot { get; set; } = true; + /// + /// Gets or sets a value indicating whether we should show notifications for errors this plugin + /// is creating. + /// + public bool NotifyForErrors { get; set; } = true; + /// /// 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; - + /// /// Gets or sets a list of validation problems that have been dismissed by the user. /// diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs index eb1f6fffc..41949c7d7 100644 --- a/Dalamud/Console/ConsoleManagerPluginScoped.cs +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -11,6 +11,47 @@ namespace Dalamud.Console; #pragma warning disable Dalamud001 +/// +/// Utility functions for the console manager. +/// +internal static partial class ConsoleManagerPluginUtil +{ + private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"]; + + /// + /// Get a sanitized namespace name from a plugin's internal name. + /// + /// The plugin's internal name. + /// A sanitized namespace. + public static string GetSanitizedNamespaceName(string pluginInternalName) + { + // Must be lowercase + pluginInternalName = pluginInternalName.ToLowerInvariant(); + + // Remove all non-alphabetic characters + pluginInternalName = NonAlphaRegex().Replace(pluginInternalName, string.Empty); + + // Remove reserved namespaces from the start or end + foreach (var reservedNamespace in ReservedNamespaces) + { + if (pluginInternalName.StartsWith(reservedNamespace)) + { + pluginInternalName = pluginInternalName[reservedNamespace.Length..]; + } + + if (pluginInternalName.EndsWith(reservedNamespace)) + { + pluginInternalName = pluginInternalName[..^reservedNamespace.Length]; + } + } + + return pluginInternalName; + } + + [GeneratedRegex(@"[^a-z]")] + private static partial Regex NonAlphaRegex(); +} + /// /// Plugin-scoped version of the console service. /// @@ -130,44 +171,3 @@ internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService return command; } } - -/// -/// Utility functions for the console manager. -/// -internal static partial class ConsoleManagerPluginUtil -{ - private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"]; - - /// - /// Get a sanitized namespace name from a plugin's internal name. - /// - /// The plugin's internal name. - /// A sanitized namespace. - public static string GetSanitizedNamespaceName(string pluginInternalName) - { - // Must be lowercase - pluginInternalName = pluginInternalName.ToLowerInvariant(); - - // Remove all non-alphabetic characters - pluginInternalName = NonAlphaRegex().Replace(pluginInternalName, string.Empty); - - // Remove reserved namespaces from the start or end - foreach (var reservedNamespace in ReservedNamespaces) - { - if (pluginInternalName.StartsWith(reservedNamespace)) - { - pluginInternalName = pluginInternalName[reservedNamespace.Length..]; - } - - if (pluginInternalName.EndsWith(reservedNamespace)) - { - pluginInternalName = pluginInternalName[..^reservedNamespace.Length]; - } - } - - return pluginInternalName; - } - - [GeneratedRegex(@"[^a-z]")] - private static partial Regex NonAlphaRegex(); -} diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 82c7f5f6c..88f9d0bb6 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -12,6 +12,7 @@ using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -47,7 +48,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework private readonly ConcurrentDictionary tickDelayedTaskCompletionSources = new(); - private ulong tickCounter; + private ulong tickCounter; [ServiceManager.ServiceConstructor] private unsafe Framework() @@ -504,14 +505,18 @@ internal sealed class Framework : IInternalDisposableService, IFramework #pragma warning restore SA1015 internal class FrameworkPluginScoped : IInternalDisposableService, IFramework { + private readonly PluginErrorHandler pluginErrorHandler; + [ServiceManager.ServiceDependency] private readonly Framework frameworkService = Service.Get(); /// /// Initializes a new instance of the class. /// - internal FrameworkPluginScoped() + /// Error handler instance. + internal FrameworkPluginScoped(PluginErrorHandler pluginErrorHandler) { + this.pluginErrorHandler = pluginErrorHandler; this.frameworkService.Update += this.OnUpdateForward; } @@ -604,7 +609,7 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework } else { - this.Update?.Invoke(framework); + this.pluginErrorHandler.InvokeAndCatch(this.Update, $"{nameof(IFramework)}::{nameof(IFramework.Update)}", framework); } } } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 8ba579d17..9760b601d 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -308,8 +308,14 @@ internal class DalamudInterface : IInternalDisposableService /// /// Opens the . /// - public void OpenLogWindow() + /// The filter to set, if not null. + public void OpenLogWindow(string? textFilter = "") { + if (textFilter != null) + { + this.consoleWindow.TextFilter = textFilter; + } + this.consoleWindow.IsOpen = true; this.consoleWindow.BringToFront(); } diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index f7ce5d145..8ef49fffc 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -41,9 +41,9 @@ internal class ConsoleWindow : Window, IDisposable // Fields below should be touched only from the main thread. private readonly RollingList logText; private readonly RollingList filteredLogEntries; - + private readonly List pluginFilters = new(); - + private readonly DalamudConfiguration configuration; private int newRolledLines; @@ -87,14 +87,14 @@ internal class ConsoleWindow : Window, IDisposable : base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { this.configuration = configuration; - + this.autoScroll = configuration.LogAutoScroll; this.autoOpen = configuration.LogOpenAtStartup; Service.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate); - + var cm = Service.Get(); - cm.AddCommand("clear", "Clear the console log", () => + cm.AddCommand("clear", "Clear the console log", () => { this.QueueClear(); return true; @@ -123,6 +123,19 @@ internal class ConsoleWindow : Window, IDisposable /// Gets the queue where log entries that are not processed yet are stored. public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new(); + /// + /// Gets or sets the current text filter. + /// + public string TextFilter + { + get => this.textFilter; + set + { + this.textFilter = value; + this.RecompileLogFilter(); + } + } + /// public override void OnOpen() { @@ -578,7 +591,7 @@ internal class ConsoleWindow : Window, IDisposable inputWidth = ImGui.GetWindowWidth() - (ImGui.GetStyle().WindowPadding.X * 2); if (!breakInputLines) - inputWidth = (inputWidth - ImGui.GetStyle().ItemSpacing.X) / 2; + inputWidth = (inputWidth - ImGui.GetStyle().ItemSpacing.X) / 2; } else { @@ -622,24 +635,29 @@ internal class ConsoleWindow : Window, IDisposable ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll) || ImGui.IsItemDeactivatedAfterEdit()) { - this.compiledLogFilter = null; - this.exceptionLogFilter = null; - try - { - this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase); - - this.QueueRefilter(); - } - catch (Exception e) - { - this.exceptionLogFilter = e; - } - - foreach (var log in this.logText) - log.HighlightMatches = null; + this.RecompileLogFilter(); } } + private void RecompileLogFilter() + { + this.compiledLogFilter = null; + this.exceptionLogFilter = null; + try + { + this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase); + + this.QueueRefilter(); + } + catch (Exception e) + { + this.exceptionLogFilter = e; + } + + foreach (var log in this.logText) + log.HighlightMatches = null; + } + private void DrawSettingsPopup() { if (ImGui.Checkbox("Open at startup", ref this.autoOpen)) @@ -799,15 +817,15 @@ internal class ConsoleWindow : Window, IDisposable { if (string.IsNullOrEmpty(this.commandText)) return; - + this.historyPos = -1; - + if (this.commandText != this.configuration.LogCommandHistory.LastOrDefault()) this.configuration.LogCommandHistory.Add(this.commandText); - + if (this.configuration.LogCommandHistory.Count > HistorySize) this.configuration.LogCommandHistory.RemoveAt(0); - + this.configuration.QueueSave(); this.lastCmdSuccess = Service.Get().ProcessCommand(this.commandText); @@ -832,7 +850,7 @@ internal class ConsoleWindow : Window, IDisposable this.completionZipText = null; this.completionTabIdx = 0; break; - + case ImGuiInputTextFlags.CallbackCompletion: var textBytes = new byte[data->BufTextLen]; Marshal.Copy((IntPtr)data->Buf, textBytes, 0, data->BufTextLen); @@ -843,11 +861,11 @@ internal class ConsoleWindow : Window, IDisposable // We can't do any completion for parameters at the moment since it just calls into CommandHandler if (words.Length > 1) return 0; - + var wordToComplete = words[0]; if (wordToComplete.IsNullOrWhitespace()) return 0; - + if (this.completionZipText is not null) wordToComplete = this.completionZipText; @@ -878,7 +896,7 @@ internal class ConsoleWindow : Window, IDisposable toComplete = candidates.ElementAt(this.completionTabIdx); this.completionTabIdx = (this.completionTabIdx + 1) % candidates.Count(); } - + if (toComplete != null) { ptr.DeleteChars(0, ptr.BufTextLen); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index cfcad2ff4..c1bd64447 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3569,6 +3569,24 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading); } + + // Error Notifications + ImGui.PushStyleColor(ImGuiCol.Button, plugin.NotifyForErrors ? greenColor : redColor); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.NotifyForErrors ? greenColor : redColor); + + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Bolt)) + { + plugin.NotifyForErrors ^= true; + configuration.QueueSave(); + } + + ImGui.PopStyleColor(2); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(Locs.PluginButtonToolTip_NotifyForErrors); + } } } @@ -4239,6 +4257,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading"); + public static string PluginButtonToolTip_NotifyForErrors => Loc.Localize("InstallerNotifyForErrors", "Show Dalamud notifications when this plugin is creating errors"); + public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin"); public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete right now - please restart the game."); diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs index 7305aa87b..5b0ca15e5 100644 --- a/Dalamud/Logging/ScopedPluginLogService.cs +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -1,5 +1,6 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; @@ -20,6 +21,7 @@ namespace Dalamud.Logging; internal class ScopedPluginLogService : IServiceType, IPluginLog { private readonly LocalPlugin localPlugin; + private readonly PluginErrorHandler errorHandler; private readonly LoggingLevelSwitch levelSwitch; @@ -27,10 +29,12 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog /// Initializes a new instance of the class. /// /// The plugin that owns this service. - internal ScopedPluginLogService(LocalPlugin localPlugin) + /// Error notifier service. + internal ScopedPluginLogService(LocalPlugin localPlugin, PluginErrorHandler errorHandler) { this.localPlugin = localPlugin; - + this.errorHandler = errorHandler; + this.levelSwitch = new LoggingLevelSwitch(this.GetDefaultLevel()); var loggerConfiguration = new LoggerConfiguration() @@ -40,7 +44,7 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog this.Logger = loggerConfiguration.CreateLogger(); } - + /// public ILogger Logger { get; } @@ -50,7 +54,7 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog get => this.levelSwitch.MinimumLevel; set => this.levelSwitch.MinimumLevel = value; } - + /// public void Fatal(string messageTemplate, params object[] values) => this.Write(LogEventLevel.Fatal, null, messageTemplate, values); @@ -82,11 +86,11 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog /// public void Information(Exception? exception, string messageTemplate, params object[] values) => this.Write(LogEventLevel.Information, exception, messageTemplate, values); - + /// public void Info(string messageTemplate, params object[] values) => this.Information(messageTemplate, values); - + /// public void Info(Exception? exception, string messageTemplate, params object[] values) => this.Information(exception, messageTemplate, values); @@ -106,10 +110,13 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog /// 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) { + if (level == LogEventLevel.Error) + this.errorHandler.NotifyError(); + this.Logger.Write( level, exception: exception, @@ -124,7 +131,7 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog 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/Internal/PluginErrorHandler.cs b/Dalamud/Plugin/Internal/PluginErrorHandler.cs new file mode 100644 index 000000000..54589595c --- /dev/null +++ b/Dalamud/Plugin/Internal/PluginErrorHandler.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; + +using ImGuiNET; + +using Serilog; + +namespace Dalamud.Plugin.Internal; + +/// +/// Service responsible for notifying the user when a plugin is creating errors. +/// +[ServiceManager.ScopedService] +internal class PluginErrorHandler : IServiceType +{ + private readonly LocalPlugin plugin; + private readonly NotificationManager notificationManager; + private readonly DalamudInterface di; + + private readonly Dictionary invokerCache = new(); + + private DateTime lastErrorTime = DateTime.MinValue; + private IActiveNotification? activeNotification; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin we are notifying for. + /// The notification manager. + /// The dalamud interface class. + [ServiceManager.ServiceConstructor] + public PluginErrorHandler(LocalPlugin plugin, NotificationManager notificationManager, DalamudInterface di) + { + this.plugin = plugin; + this.notificationManager = notificationManager; + this.di = di; + } + + /// + /// Invoke the specified delegate and catch any exceptions that occur. + /// Writes an error message to the log if an exception occurs and shows + /// a notification if the plugin is a dev plugin and the user has enabled error notifications. + /// + /// The delegate to invoke. + /// A hint to show about the origin of the exception if an error occurs. + /// Arguments to the event handler. + /// The type of the delegate. + /// Whether invocation was successful/did not throw an exception. + public bool InvokeAndCatch( + TDelegate? eventHandler, + string hint, + params object[] args) + where TDelegate : Delegate + { + if (eventHandler == null) + return true; + + try + { + var invoker = this.GetInvoker(); + invoker(eventHandler, args); + return true; + } + catch (Exception ex) + { + Log.Error(ex, $"[{this.plugin.InternalName}] Exception in event handler {{EventHandlerName}}", hint); + this.NotifyError(); + return false; + } + } + + /// + /// Show a notification, if the plugin is a dev plugin and the user has enabled error notifications. + /// This function has a cooldown built-in. + /// + public void NotifyError() + { + if (this.plugin is not LocalDevPlugin devPlugin) + return; + + if (!devPlugin.NotifyForErrors) + return; + + // If the notification is already active, we don't need to show it again. + if (this.activeNotification is { DismissReason: null }) + return; + + var now = DateTime.UtcNow; + if (now - this.lastErrorTime < TimeSpan.FromMinutes(2)) + return; + + this.lastErrorTime = now; + + var creatingErrorsText = $"{devPlugin.Name} is creating errors"; + var notification = new Notification() + { + Title = creatingErrorsText, + Icon = INotificationIcon.From(FontAwesomeIcon.Bolt), + Type = NotificationType.Error, + InitialDuration = TimeSpan.FromSeconds(15), + MinimizedText = creatingErrorsText, + Content = $"The plugin '{devPlugin.Name}' is creating errors. Click 'Show console' to learn more.", + RespectUiHidden = false, + }; + + this.activeNotification = this.notificationManager.AddNotification(notification); + this.activeNotification.DrawActions += _ => + { + if (ImGui.Button("Show console")) + { + this.di.OpenLogWindow(this.plugin.InternalName); + this.activeNotification.DismissNow(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Show the console filtered to this plugin"); + } + + ImGui.SameLine(); + + if (ImGui.Button("Disable notifications")) + { + devPlugin.NotifyForErrors = false; + this.activeNotification.DismissNow(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Disable error notifications for this plugin"); + } + }; + } + + private static Action CreateInvoker() where TDelegate : Delegate + { + var delegateType = typeof(TDelegate); + var method = delegateType.GetMethod("Invoke"); + if (method == null) + throw new InvalidOperationException($"Delegate {delegateType} does not have an Invoke method."); + + var parameters = method.GetParameters(); + + // Create parameters for the lambda + var delegateParam = Expression.Parameter(delegateType, "d"); + var argsParam = Expression.Parameter(typeof(object[]), "args"); + + // Create expressions to convert array elements to parameter types + var callArgs = new Expression[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + var paramType = parameters[i].ParameterType; + var arrayAccess = Expression.ArrayIndex(argsParam, Expression.Constant(i)); + callArgs[i] = Expression.Convert(arrayAccess, paramType); + } + + // Create the delegate invocation expression + var callExpr = Expression.Call(delegateParam, method, callArgs); + + // If return type is not void, discard the result + Expression bodyExpr; + if (method.ReturnType != typeof(void)) + { + // Create a block that executes the call and then returns void + bodyExpr = Expression.Block( + Expression.Call(delegateParam, method, callArgs), + Expression.Empty()); + } + else + { + bodyExpr = callExpr; + } + + // Compile and return the lambda + var lambda = Expression.Lambda>( + bodyExpr, delegateParam, argsParam); + return lambda.Compile(); + } + + private Action GetInvoker() where TDelegate : Delegate + { + var delegateType = typeof(TDelegate); + + if (!this.invokerCache.TryGetValue(delegateType, out var cachedInvoker)) + { + cachedInvoker = CreateInvoker(); + this.invokerCache[delegateType] = cachedInvoker; + } + + return (Action)cachedInvoker; + } +} diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index b8f2b2708..34b54163a 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -86,6 +86,16 @@ internal sealed class LocalDevPlugin : LocalPlugin } } + /// + /// Gets or sets a value indicating whether users should be notified when this plugin + /// is causing errors. + /// + public bool NotifyForErrors + { + get => this.devSettings.NotifyForErrors; + set => this.devSettings.NotifyForErrors = value; + } + /// /// Gets an ID uniquely identifying this specific instance of a devPlugin. /// diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 92fe5ae41..9847f7147 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -152,7 +152,7 @@ internal static class ServiceManager #if DEBUG lock (LoadedServices) { - ProvideAllServices() + ProvideAllServices(); } return; diff --git a/Dalamud/Service/Service{T}.cs b/Dalamud/Service/Service{T}.cs index c92c8baff..1f5558893 100644 --- a/Dalamud/Service/Service{T}.cs +++ b/Dalamud/Service/Service{T}.cs @@ -23,6 +23,9 @@ namespace Dalamud; [SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")] internal static class Service where T : IServiceType { + // TODO: Service should only work with singleton services. Trying to call Service.Get() on a scoped service should + // be a compile-time error. + private static readonly ServiceManager.ServiceAttribute ServiceAttribute; private static TaskCompletionSource instanceTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); private static List? dependencyServices;