mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
Add plugin error notifications, per-plugin event invocation wrappers
This commit is contained in:
parent
1913a4cd2c
commit
ddf0a97c83
11 changed files with 358 additions and 85 deletions
|
|
@ -12,6 +12,12 @@ internal sealed class DevPluginSettings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool StartOnBoot { get; set; } = true;
|
public bool StartOnBoot { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether we should show notifications for errors this plugin
|
||||||
|
/// is creating.
|
||||||
|
/// </summary>
|
||||||
|
public bool NotifyForErrors { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether this plugin should automatically reload on file change.
|
/// Gets or sets a value indicating whether this plugin should automatically reload on file change.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,47 @@ namespace Dalamud.Console;
|
||||||
|
|
||||||
#pragma warning disable Dalamud001
|
#pragma warning disable Dalamud001
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Utility functions for the console manager.
|
||||||
|
/// </summary>
|
||||||
|
internal static partial class ConsoleManagerPluginUtil
|
||||||
|
{
|
||||||
|
private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a sanitized namespace name from a plugin's internal name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pluginInternalName">The plugin's internal name.</param>
|
||||||
|
/// <returns>A sanitized namespace.</returns>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plugin-scoped version of the console service.
|
/// Plugin-scoped version of the console service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -130,44 +171,3 @@ internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Utility functions for the console manager.
|
|
||||||
/// </summary>
|
|
||||||
internal static partial class ConsoleManagerPluginUtil
|
|
||||||
{
|
|
||||||
private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a sanitized namespace name from a plugin's internal name.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="pluginInternalName">The plugin's internal name.</param>
|
|
||||||
/// <returns>A sanitized namespace.</returns>
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ using Dalamud.Hooking;
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
using Dalamud.Logging.Internal;
|
using Dalamud.Logging.Internal;
|
||||||
|
using Dalamud.Plugin.Internal;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
|
|
||||||
|
|
@ -504,14 +505,18 @@ internal sealed class Framework : IInternalDisposableService, IFramework
|
||||||
#pragma warning restore SA1015
|
#pragma warning restore SA1015
|
||||||
internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
|
internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
|
||||||
{
|
{
|
||||||
|
private readonly PluginErrorHandler pluginErrorHandler;
|
||||||
|
|
||||||
[ServiceManager.ServiceDependency]
|
[ServiceManager.ServiceDependency]
|
||||||
private readonly Framework frameworkService = Service<Framework>.Get();
|
private readonly Framework frameworkService = Service<Framework>.Get();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="FrameworkPluginScoped"/> class.
|
/// Initializes a new instance of the <see cref="FrameworkPluginScoped"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal FrameworkPluginScoped()
|
/// <param name="pluginErrorHandler">Error handler instance.</param>
|
||||||
|
internal FrameworkPluginScoped(PluginErrorHandler pluginErrorHandler)
|
||||||
{
|
{
|
||||||
|
this.pluginErrorHandler = pluginErrorHandler;
|
||||||
this.frameworkService.Update += this.OnUpdateForward;
|
this.frameworkService.Update += this.OnUpdateForward;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -604,7 +609,7 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.Update?.Invoke(framework);
|
this.pluginErrorHandler.InvokeAndCatch(this.Update, $"{nameof(IFramework)}::{nameof(IFramework.Update)}", framework);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -308,8 +308,14 @@ internal class DalamudInterface : IInternalDisposableService
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Opens the <see cref="ConsoleWindow"/>.
|
/// Opens the <see cref="ConsoleWindow"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void OpenLogWindow()
|
/// <param name="textFilter">The filter to set, if not null.</param>
|
||||||
|
public void OpenLogWindow(string? textFilter = "")
|
||||||
{
|
{
|
||||||
|
if (textFilter != null)
|
||||||
|
{
|
||||||
|
this.consoleWindow.TextFilter = textFilter;
|
||||||
|
}
|
||||||
|
|
||||||
this.consoleWindow.IsOpen = true;
|
this.consoleWindow.IsOpen = true;
|
||||||
this.consoleWindow.BringToFront();
|
this.consoleWindow.BringToFront();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,19 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
/// <summary>Gets the queue where log entries that are not processed yet are stored.</summary>
|
/// <summary>Gets the queue where log entries that are not processed yet are stored.</summary>
|
||||||
public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
|
public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the current text filter.
|
||||||
|
/// </summary>
|
||||||
|
public string TextFilter
|
||||||
|
{
|
||||||
|
get => this.textFilter;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
this.textFilter = value;
|
||||||
|
this.RecompileLogFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void OnOpen()
|
public override void OnOpen()
|
||||||
{
|
{
|
||||||
|
|
@ -622,24 +635,29 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
|
ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
|
||||||
|| ImGui.IsItemDeactivatedAfterEdit())
|
|| ImGui.IsItemDeactivatedAfterEdit())
|
||||||
{
|
{
|
||||||
this.compiledLogFilter = null;
|
this.RecompileLogFilter();
|
||||||
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 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()
|
private void DrawSettingsPopup()
|
||||||
{
|
{
|
||||||
if (ImGui.Checkbox("Open at startup", ref this.autoOpen))
|
if (ImGui.Checkbox("Open at startup", ref this.autoOpen))
|
||||||
|
|
|
||||||
|
|
@ -3569,6 +3569,24 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
{
|
{
|
||||||
ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading);
|
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_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_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin");
|
||||||
|
|
||||||
public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete right now - please restart the game.");
|
public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete right now - please restart the game.");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
|
using Dalamud.Plugin.Internal;
|
||||||
using Dalamud.Plugin.Internal.Types;
|
using Dalamud.Plugin.Internal.Types;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
|
@ -20,6 +21,7 @@ namespace Dalamud.Logging;
|
||||||
internal class ScopedPluginLogService : IServiceType, IPluginLog
|
internal class ScopedPluginLogService : IServiceType, IPluginLog
|
||||||
{
|
{
|
||||||
private readonly LocalPlugin localPlugin;
|
private readonly LocalPlugin localPlugin;
|
||||||
|
private readonly PluginErrorHandler errorHandler;
|
||||||
|
|
||||||
private readonly LoggingLevelSwitch levelSwitch;
|
private readonly LoggingLevelSwitch levelSwitch;
|
||||||
|
|
||||||
|
|
@ -27,9 +29,11 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog
|
||||||
/// Initializes a new instance of the <see cref="ScopedPluginLogService"/> class.
|
/// Initializes a new instance of the <see cref="ScopedPluginLogService"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="localPlugin">The plugin that owns this service.</param>
|
/// <param name="localPlugin">The plugin that owns this service.</param>
|
||||||
internal ScopedPluginLogService(LocalPlugin localPlugin)
|
/// <param name="errorHandler">Error notifier service.</param>
|
||||||
|
internal ScopedPluginLogService(LocalPlugin localPlugin, PluginErrorHandler errorHandler)
|
||||||
{
|
{
|
||||||
this.localPlugin = localPlugin;
|
this.localPlugin = localPlugin;
|
||||||
|
this.errorHandler = errorHandler;
|
||||||
|
|
||||||
this.levelSwitch = new LoggingLevelSwitch(this.GetDefaultLevel());
|
this.levelSwitch = new LoggingLevelSwitch(this.GetDefaultLevel());
|
||||||
|
|
||||||
|
|
@ -110,6 +114,9 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Write(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values)
|
public void Write(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values)
|
||||||
{
|
{
|
||||||
|
if (level == LogEventLevel.Error)
|
||||||
|
this.errorHandler.NotifyError();
|
||||||
|
|
||||||
this.Logger.Write(
|
this.Logger.Write(
|
||||||
level,
|
level,
|
||||||
exception: exception,
|
exception: exception,
|
||||||
|
|
|
||||||
198
Dalamud/Plugin/Internal/PluginErrorHandler.cs
Normal file
198
Dalamud/Plugin/Internal/PluginErrorHandler.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service responsible for notifying the user when a plugin is creating errors.
|
||||||
|
/// </summary>
|
||||||
|
[ServiceManager.ScopedService]
|
||||||
|
internal class PluginErrorHandler : IServiceType
|
||||||
|
{
|
||||||
|
private readonly LocalPlugin plugin;
|
||||||
|
private readonly NotificationManager notificationManager;
|
||||||
|
private readonly DalamudInterface di;
|
||||||
|
|
||||||
|
private readonly Dictionary<Type, Delegate> invokerCache = new();
|
||||||
|
|
||||||
|
private DateTime lastErrorTime = DateTime.MinValue;
|
||||||
|
private IActiveNotification? activeNotification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PluginErrorHandler"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plugin">The plugin we are notifying for.</param>
|
||||||
|
/// <param name="notificationManager">The notification manager.</param>
|
||||||
|
/// <param name="di">The dalamud interface class.</param>
|
||||||
|
[ServiceManager.ServiceConstructor]
|
||||||
|
public PluginErrorHandler(LocalPlugin plugin, NotificationManager notificationManager, DalamudInterface di)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.notificationManager = notificationManager;
|
||||||
|
this.di = di;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="eventHandler">The delegate to invoke.</param>
|
||||||
|
/// <param name="hint">A hint to show about the origin of the exception if an error occurs.</param>
|
||||||
|
/// <param name="args">Arguments to the event handler.</param>
|
||||||
|
/// <typeparam name="TDelegate">The type of the delegate.</typeparam>
|
||||||
|
/// <returns>Whether invocation was successful/did not throw an exception.</returns>
|
||||||
|
public bool InvokeAndCatch<TDelegate>(
|
||||||
|
TDelegate? eventHandler,
|
||||||
|
string hint,
|
||||||
|
params object[] args)
|
||||||
|
where TDelegate : Delegate
|
||||||
|
{
|
||||||
|
if (eventHandler == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var invoker = this.GetInvoker<TDelegate>();
|
||||||
|
invoker(eventHandler, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, $"[{this.plugin.InternalName}] Exception in event handler {{EventHandlerName}}", hint);
|
||||||
|
this.NotifyError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show a notification, if the plugin is a dev plugin and the user has enabled error notifications.
|
||||||
|
/// This function has a cooldown built-in.
|
||||||
|
/// </summary>
|
||||||
|
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<TDelegate, object[]> CreateInvoker<TDelegate>() 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<Action<TDelegate, object[]>>(
|
||||||
|
bodyExpr, delegateParam, argsParam);
|
||||||
|
return lambda.Compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Action<TDelegate, object[]> GetInvoker<TDelegate>() where TDelegate : Delegate
|
||||||
|
{
|
||||||
|
var delegateType = typeof(TDelegate);
|
||||||
|
|
||||||
|
if (!this.invokerCache.TryGetValue(delegateType, out var cachedInvoker))
|
||||||
|
{
|
||||||
|
cachedInvoker = CreateInvoker<TDelegate>();
|
||||||
|
this.invokerCache[delegateType] = cachedInvoker;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Action<TDelegate, object[]>)cachedInvoker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -86,6 +86,16 @@ internal sealed class LocalDevPlugin : LocalPlugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether users should be notified when this plugin
|
||||||
|
/// is causing errors.
|
||||||
|
/// </summary>
|
||||||
|
public bool NotifyForErrors
|
||||||
|
{
|
||||||
|
get => this.devSettings.NotifyForErrors;
|
||||||
|
set => this.devSettings.NotifyForErrors = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an ID uniquely identifying this specific instance of a devPlugin.
|
/// Gets an ID uniquely identifying this specific instance of a devPlugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ internal static class ServiceManager
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
lock (LoadedServices)
|
lock (LoadedServices)
|
||||||
{
|
{
|
||||||
ProvideAllServices()
|
ProvideAllServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ namespace Dalamud;
|
||||||
[SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")]
|
[SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")]
|
||||||
internal static class Service<T> where T : IServiceType
|
internal static class Service<T> where T : IServiceType
|
||||||
{
|
{
|
||||||
|
// TODO: Service<T> should only work with singleton services. Trying to call Service<T>.Get() on a scoped service should
|
||||||
|
// be a compile-time error.
|
||||||
|
|
||||||
private static readonly ServiceManager.ServiceAttribute ServiceAttribute;
|
private static readonly ServiceManager.ServiceAttribute ServiceAttribute;
|
||||||
private static TaskCompletionSource<T> instanceTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
private static TaskCompletionSource<T> instanceTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
private static List<Type>? dependencyServices;
|
private static List<Type>? dependencyServices;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue