This commit is contained in:
goaaats 2025-06-22 21:39:38 +02:00
commit 95ec633cc5
163 changed files with 7036 additions and 1585 deletions

View file

@ -129,7 +129,7 @@ internal class AutoUpdateManager : IServiceType
}
/// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session.
/// Gets a value indicating whether auto-updates have already completed this session.
/// </summary>
public bool IsAutoUpdateComplete { get; private set; }
@ -458,7 +458,7 @@ internal class AutoUpdateManager : IServiceType
.Where(
p =>
!p.InstalledPlugin.IsDev && // Never update dev-plugins
p.InstalledPlugin.IsWantedByAnyProfile && // Never update plugins that are not wanted by any profile(not enabled)
(p.InstalledPlugin.IsWantedByAnyProfile || this.config.UpdateDisabledPlugins) && // Never update plugins that are not wanted by any profile(not enabled)
!p.InstalledPlugin.Manifest.ScheduledForDeletion); // Never update plugins that we want to get rid of
return updateablePlugins.Where(FilterPlugin).ToList();
@ -499,7 +499,7 @@ internal class AutoUpdateManager : IServiceType
condition.OnlyAny(ConditionFlag.NormalConditions,
ConditionFlag.Jumping,
ConditionFlag.Mounted,
ConditionFlag.UsingParasol);
ConditionFlag.UsingFashionAccessory);
}
private bool IsPluginManagerReady()

View file

@ -0,0 +1,199 @@
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.\n\n" +
$"You are seeing this because '{devPlugin.Name}' is a Dev Plugin.",
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;
}
}

View file

@ -284,7 +284,7 @@ internal class PluginManager : IInternalDisposableService
/// Check if a manifest even has an available testing version.
/// </summary>
/// <param name="manifest">The manifest to test.</param>
/// <returns>Whether or not a testing version is available.</returns>
/// <returns>Whether a testing version is available.</returns>
public static bool HasTestingVersion(IPluginManifest manifest)
{
var av = manifest.AssemblyVersion;
@ -663,6 +663,8 @@ internal class PluginManager : IInternalDisposableService
_ = Task.Run(
async () =>
{
Log.Verbose("Starting async boot load");
// Load plugins that want to be loaded during Framework.Tick
var framework = await Service<Framework>.GetAsync().ConfigureAwait(false);
await framework.RunOnTick(
@ -671,27 +673,35 @@ internal class PluginManager : IInternalDisposableService
syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1),
tokenSource.Token),
cancellationToken: tokenSource.Token).ConfigureAwait(false);
Log.Verbose("Loaded FrameworkTickSync plugins (LoadRequiredState == 1)");
loadTasks.Add(LoadPluginsAsync(
"FrameworkTickAsync",
asyncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1),
tokenSource.Token));
Log.Verbose("Kicked off FrameworkTickAsync plugins (LoadRequiredState == 1)");
// Load plugins that want to be loaded during Framework.Tick, when drawing facilities are available
_ = await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync().ConfigureAwait(false);
Log.Verbose(" InterfaceManager is ready, starting to load DrawAvailableSync plugins");
await framework.RunOnTick(
() => LoadPluginsSync(
"DrawAvailableSync",
syncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null),
tokenSource.Token),
cancellationToken: tokenSource.Token);
Log.Verbose("Loaded DrawAvailableSync plugins (LoadRequiredState == 0 or null)");
loadTasks.Add(LoadPluginsAsync(
"DrawAvailableAsync",
asyncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null),
tokenSource.Token));
Log.Verbose("Kicked off DrawAvailableAsync plugins (LoadRequiredState == 0 or null)");
// Save signatures when all plugins are done loading, successful or not.
try
{
Log.Verbose("Now waiting for {NumTasks} async load tasks", loadTasks.Count);
await Task.WhenAll(loadTasks).ConfigureAwait(false);
Log.Information("Loaded plugins on boot");
}
@ -715,8 +725,13 @@ internal class PluginManager : IInternalDisposableService
}
this.StartupLoadTracking = null;
},
tokenSource.Token);
}, tokenSource.Token).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception, "Failed to load FrameworkTickAsync/DrawAvailableAsync plugins");
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
/// <summary>
@ -776,7 +791,8 @@ internal class PluginManager : IInternalDisposableService
/// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works
/// a little differently.
/// </summary>
public void ScanDevPlugins()
/// <returns>A <see cref="Task"/> representing the asynchronous operation. This function generally will not block as new plugins aren't loaded.</returns>
public async Task ScanDevPluginsAsync()
{
// devPlugins are more freeform. Look for any dll and hope to get lucky.
var devDllFiles = new List<FileInfo>();
@ -823,8 +839,7 @@ internal class PluginManager : IInternalDisposableService
try
{
// Add them to the list and let the user decide, nothing is auto-loaded.
this.LoadPluginAsync(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true)
.Wait();
await this.LoadPluginAsync(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true);
listChanged = true;
}
catch (InvalidPluginException)
@ -1037,7 +1052,7 @@ internal class PluginManager : IInternalDisposableService
/// </summary>
/// <param name="metadata">The available plugin update.</param>
/// <param name="notify">Whether to notify that installed plugins have changed afterwards.</param>
/// <param name="dryRun">Whether or not to actually perform the update, or just indicate success.</param>
/// <param name="dryRun">Whether to actually perform the update, or just indicate success.</param>
/// <returns>The status of the update.</returns>
public async Task<PluginUpdateStatus> UpdateSinglePluginAsync(AvailablePluginUpdate metadata, bool notify, bool dryRun)
{
@ -1188,32 +1203,20 @@ internal class PluginManager : IInternalDisposableService
{
// Testing exclusive
if (manifest.IsTestingExclusive && !this.configuration.DoPluginTest)
{
Log.Verbose($"Testing exclusivity: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}");
return false;
}
// Applicable version
if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion)
{
Log.Verbose($"Game version: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}");
return false;
}
// API level - we keep the API before this in the installer to show as "outdated"
var effectiveApiLevel = this.UseTesting(manifest) && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel;
if (effectiveApiLevel < DalamudApiLevel - 1 && !this.LoadAllApiLevels)
{
Log.Verbose($"API Level: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}");
return false;
}
// Banned
if (this.IsManifestBanned(manifest))
{
Log.Verbose($"Banned: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}");
return false;
}
return true;
}
@ -1572,6 +1575,8 @@ internal class PluginManager : IInternalDisposableService
/// <returns>The loaded plugin.</returns>
private async Task<LocalPlugin> LoadPluginAsync(FileInfo dllFile, LocalPluginManifest manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false)
{
// TODO: Split this function - it should only take care of adding the plugin to the list, not loading itself, that should be done through the plugin instance
var loadPlugin = !doNotLoad;
LocalPlugin? plugin;
@ -1582,21 +1587,34 @@ internal class PluginManager : IInternalDisposableService
throw new Exception("No internal name");
}
if (isDev)
// Track the plugin as soon as it is instantiated to prevent it from being loaded twice,
// if the installer or DevPlugin scanner is attempting to add plugins while we are still loading boot plugins
lock (this.pluginListLock)
{
Log.Information("Loading dev plugin {Name}", manifest.InternalName);
plugin = new LocalDevPlugin(dllFile, manifest);
// Check if this plugin is already loaded
if (this.installedPluginsList.Any(lp => lp.DllFile.FullName == dllFile.FullName))
throw new InvalidOperationException("Plugin at the provided path is already loaded");
// This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet
// TODO(goat): Re-enable this when we have better tracing for what was rendering when
// this.configuration.ImGuiAssertsEnabledAtStartup ??= true;
}
else
{
Log.Information("Loading plugin {Name}", manifest.InternalName);
plugin = new LocalPlugin(dllFile, manifest);
if (isDev)
{
Log.Information("Loading dev plugin {Name}", manifest.InternalName);
plugin = new LocalDevPlugin(dllFile, manifest);
// This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet
// TODO(goat): Re-enable this when we have better tracing for what was rendering when
// this.configuration.ImGuiAssertsEnabledAtStartup ??= true;
}
else
{
Log.Information("Loading plugin {Name}", manifest.InternalName);
plugin = new LocalPlugin(dllFile, manifest);
}
this.installedPluginsList.Add(plugin);
}
Log.Verbose("Starting to load plugin {Name} at {FileLocation}", manifest.InternalName, dllFile.FullName);
// Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here.
// This will also happen if you are installing a plugin with the installer, and that's intended!
// It means that, if you have a profile which has unsatisfied plugins, installing a matching plugin will
@ -1697,43 +1715,34 @@ internal class PluginManager : IInternalDisposableService
catch (BannedPluginException)
{
// Out of date plugins get added so they can be updated.
Log.Information($"Plugin was banned, adding anyways: {dllFile.Name}");
Log.Information("{InternalName}: Plugin was banned, adding anyways", plugin.Manifest.InternalName);
}
catch (Exception ex)
{
if (plugin.IsDev)
{
// Dev plugins always get added to the list so they can be fiddled with in the UI
Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}");
// NOTE(goat): This can't work - plugins don't "unload" if they fail to load.
// plugin.Disable(); // Disable here, otherwise you can't enable+load later
Log.Information(ex, "{InternalName}: Dev plugin failed to load", plugin.Manifest.InternalName);
}
else if (plugin.IsOutdated)
{
// Out of date plugins get added, so they can be updated.
Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}");
Log.Information(ex, "{InternalName}: Plugin was outdated", plugin.Manifest.InternalName);
}
else if (plugin.IsOrphaned)
{
// Orphaned plugins get added, so that users aren't confused.
Log.Information(ex, $"Plugin was orphaned, adding anyways: {dllFile.Name}");
Log.Information(ex, "{InternalName}: Plugin was orphaned", plugin.Manifest.InternalName);
}
else if (isBoot)
{
// During boot load, plugins always get added to the list so they can be fiddled with in the UI
Log.Information(ex, $"Regular plugin failed to load, adding anyways: {dllFile.Name}");
// NOTE(goat): This can't work - plugins don't "unload" if they fail to load.
// plugin.Disable(); // Disable here, otherwise you can't enable+load later
Log.Information(ex, "{InternalName}: Regular plugin failed to load", plugin.Manifest.InternalName);
}
else if (!plugin.CheckPolicy())
{
// During boot load, plugins always get added to the list so they can be fiddled with in the UI
Log.Information(ex, $"Plugin not loaded due to policy, adding anyways: {dllFile.Name}");
// NOTE(goat): This can't work - plugins don't "unload" if they fail to load.
// plugin.Disable(); // Disable here, otherwise you can't enable+load later
Log.Information(ex, "{InternalName}: Plugin not loaded due to policy", plugin.Manifest.InternalName);
}
else
{
@ -1742,14 +1751,6 @@ internal class PluginManager : IInternalDisposableService
}
}
if (plugin == null)
throw new Exception("Plugin was null when adding to list");
lock (this.pluginListLock)
{
this.installedPluginsList.Add(plugin);
}
// Mark as finished loading
if (manifest.LoadSync)
this.StartupLoadTracking?.Finish(manifest.InternalName);
@ -1774,6 +1775,7 @@ internal class PluginManager : IInternalDisposableService
var updates = this.AvailablePlugins
.Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName)
.Where(remoteManifest => plugin.Manifest.InstalledFromUrl == remoteManifest.SourceRepo.PluginMasterUrl || !remoteManifest.SourceRepo.IsThirdParty)
.Where(remoteManifest => remoteManifest.MinimumDalamudVersion == null || Util.AssemblyVersionParsed >= remoteManifest.MinimumDalamudVersion)
.Where(remoteManifest =>
{
var useTesting = this.UseTesting(remoteManifest);
@ -1844,18 +1846,27 @@ internal class PluginManager : IInternalDisposableService
_ = this.SetPluginReposFromConfigAsync(false);
this.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting);
Log.Information("[T3] PM repos OK!");
Log.Information("Repos loaded!");
}
using (Timings.Start("PM Cleanup Plugins"))
{
this.CleanupPlugins();
Log.Information("[T3] PMC OK!");
Log.Information("Plugin cleanup OK!");
}
using (Timings.Start("PM Load Sync Plugins"))
{
var loadAllPlugins = Task.Run(this.LoadAllPlugins);
var loadAllPlugins = Task.Run(this.LoadAllPlugins)
.ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception, "Error in LoadAllPlugins()");
}
_ = Task.Run(Troubleshooting.LogTroubleshooting);
});
// We wait for all blocking services and tasks to finish before kicking off the main thread in any mode.
// This means that we don't want to block here if this stupid thing isn't enabled.
@ -1865,10 +1876,8 @@ internal class PluginManager : IInternalDisposableService
loadAllPlugins.Wait();
}
Log.Information("[T3] PML OK!");
Log.Information("Boot load started");
}
_ = Task.Run(Troubleshooting.LogTroubleshooting);
}
catch (Exception ex)
{

View file

@ -24,8 +24,8 @@ internal class Profile
/// </summary>
/// <param name="manager">The manager this profile belongs to.</param>
/// <param name="model">The model this profile is tied to.</param>
/// <param name="isDefaultProfile">Whether or not this profile is the default profile.</param>
/// <param name="isBoot">Whether or not this profile was initialized during bootup.</param>
/// <param name="isDefaultProfile">Whether this profile is the default profile.</param>
/// <param name="isBoot">Whether this profile was initialized during bootup.</param>
public Profile(ProfileManager manager, ProfileModel model, bool isDefaultProfile, bool isBoot)
{
this.manager = manager;
@ -33,6 +33,18 @@ internal class Profile
this.modelV1 = model as ProfileModelV1 ??
throw new ArgumentException("Model was null or unhandled version");
// Migrate "policy"
if (this.modelV1.StartupPolicy == null)
{
#pragma warning disable CS0618
this.modelV1.StartupPolicy = this.modelV1.AlwaysEnableOnBoot
? ProfileModelV1.ProfileStartupPolicy.AlwaysEnable
: ProfileModelV1.ProfileStartupPolicy.RememberState;
#pragma warning restore CS0618
Service<DalamudConfiguration>.Get().QueueSave();
}
// We don't actually enable plugins here, PM will do it on bootup
if (isDefaultProfile)
{
@ -40,20 +52,40 @@ internal class Profile
this.IsEnabled = this.modelV1.IsEnabled = true;
this.Name = this.modelV1.Name = "DEFAULT";
}
else if (this.modelV1.AlwaysEnableOnBoot && isBoot)
else if (isBoot)
{
this.IsEnabled = true;
Log.Verbose("{Guid} set enabled because bootup", this.modelV1.Guid);
}
else if (this.modelV1.IsEnabled)
{
this.IsEnabled = true;
Log.Verbose("{Guid} set enabled because remember", this.modelV1.Guid);
if (this.modelV1.StartupPolicy == ProfileModelV1.ProfileStartupPolicy.AlwaysEnable)
{
this.IsEnabled = true;
Log.Verbose("{Guid} set enabled because always enable", this.modelV1.Guid);
}
else if (this.modelV1.StartupPolicy == ProfileModelV1.ProfileStartupPolicy.AlwaysDisable)
{
this.IsEnabled = false;
Log.Verbose("{Guid} set disabled because always disable", this.modelV1.Guid);
}
else if (this.modelV1.StartupPolicy == ProfileModelV1.ProfileStartupPolicy.RememberState)
{
this.IsEnabled = this.modelV1.IsEnabled;
Log.Verbose("{Guid} set enabled because remember", this.modelV1.Guid);
}
else
{
throw new ArgumentOutOfRangeException(nameof(this.modelV1.StartupPolicy));
}
}
else
{
Log.Verbose("{Guid} not enabled", this.modelV1.Guid);
}
Log.Verbose("Init profile {Guid} ({Name}) enabled:{Enabled} policy:{Policy} plugins:{NumPlugins} will be enabled:{Status}",
this.modelV1.Guid,
this.modelV1.Name,
this.modelV1.IsEnabled,
this.modelV1.StartupPolicy,
this.modelV1.Plugins.Count,
this.IsEnabled);
}
/// <summary>
@ -72,12 +104,12 @@ internal class Profile
/// <summary>
/// Gets or sets a value indicating whether this profile shall always be enabled at boot.
/// </summary>
public bool AlwaysEnableAtBoot
public ProfileModelV1.ProfileStartupPolicy StartupPolicy
{
get => this.modelV1.AlwaysEnableOnBoot;
get => this.modelV1.StartupPolicy ?? ProfileModelV1.ProfileStartupPolicy.RememberState;
set
{
this.modelV1.AlwaysEnableOnBoot = value;
this.modelV1.StartupPolicy = value;
Service<DalamudConfiguration>.Get().QueueSave();
}
}
@ -88,12 +120,12 @@ internal class Profile
public Guid Guid => this.modelV1.Guid;
/// <summary>
/// Gets a value indicating whether or not this profile is currently enabled.
/// Gets a value indicating whether this profile is currently enabled.
/// </summary>
public bool IsEnabled { get; private set; }
/// <summary>
/// Gets a value indicating whether or not this profile is the default profile.
/// Gets a value indicating whether this profile is the default profile.
/// </summary>
public bool IsDefaultProfile { get; }
@ -119,8 +151,8 @@ internal class Profile
/// Set this profile's state. This cannot be called for the default profile.
/// This will block until all states have been applied.
/// </summary>
/// <param name="enabled">Whether or not the profile is enabled.</param>
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
/// <param name="enabled">Whether the profile is enabled.</param>
/// <param name="apply">Whether the current state should immediately be applied.</param>
/// <exception cref="InvalidOperationException">Thrown when an untoggleable profile is toggled.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task SetStateAsync(bool enabled, bool apply = true)
@ -158,13 +190,13 @@ internal class Profile
/// </summary>
/// <param name="workingPluginId">The ID of the plugin.</param>
/// <param name="internalName">The internal name of the plugin, if available.</param>
/// <param name="state">Whether or not the plugin should be enabled.</param>
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
/// <param name="state">Whether the plugin should be enabled.</param>
/// <param name="apply">Whether the current state should immediately be applied.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task AddOrUpdateAsync(Guid workingPluginId, string? internalName, bool state, bool apply = true)
{
Debug.Assert(workingPluginId != Guid.Empty, "Trying to add plugin with empty guid");
lock (this)
{
var existing = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId);
@ -182,9 +214,9 @@ internal class Profile
});
}
}
Log.Information("Adding plugin {Plugin}({Guid}) to profile {Profile} with state {State}", internalName, workingPluginId, this.Guid, state);
// We need to remove this plugin from the default profile, if it declares it.
if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(workingPluginId) != null)
{
@ -203,9 +235,9 @@ internal class Profile
/// This will block until all states have been applied.
/// </summary>
/// <param name="workingPluginId">The ID of the plugin.</param>
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
/// <param name="apply">Whether the current state should immediately be applied.</param>
/// <param name="checkDefault">
/// Whether or not to throw when a plugin is removed from the default profile, without being in another profile.
/// Whether to throw when a plugin is removed from the default profile, without being in another profile.
/// Used to prevent orphan plugins, but can be ignored when cleaning up old entries.
/// </param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
@ -221,7 +253,7 @@ internal class Profile
if (!this.modelV1.Plugins.Remove(entry))
throw new Exception("Couldn't remove plugin from model collection");
}
Log.Information("Removing plugin {Plugin}({Guid}) from profile {Profile}", entry.InternalName, entry.WorkingPluginId, this.Guid);
// We need to add this plugin back to the default profile, if we were the last profile to have it.
@ -260,7 +292,7 @@ internal class Profile
// TODO: What should happen if a profile has a GUID locked in, but the plugin
// is not installed anymore? That probably means that the user uninstalled the plugin
// and is now reinstalling it. We should still satisfy that and update the ID.
if (plugin.InternalName == internalName && plugin.WorkingPluginId == Guid.Empty)
{
plugin.WorkingPluginId = newGuid;
@ -268,7 +300,7 @@ internal class Profile
}
}
}
Service<DalamudConfiguration>.Get().QueueSave();
}
@ -319,7 +351,7 @@ internal sealed class PluginNotFoundException : ProfileOperationException
: base($"The plugin '{internalName}' was not found in the profile")
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PluginNotFoundException"/> class.
/// </summary>

View file

@ -54,7 +54,7 @@ internal class ProfileManager : IServiceType
public IEnumerable<Profile> Profiles => this.profiles;
/// <summary>
/// Gets a value indicating whether or not the profile manager is busy enabling/disabling plugins.
/// Gets a value indicating whether the profile manager is busy enabling/disabling plugins.
/// </summary>
public bool IsBusy => this.isBusy;
@ -71,8 +71,8 @@ internal class ProfileManager : IServiceType
/// <param name="workingPluginId">The ID of the plugin.</param>
/// <param name="internalName">The internal name of the plugin, if available.</param>
/// <param name="defaultState">The state the plugin shall be in, if it needs to be added.</param>
/// <param name="addIfNotDeclared">Whether or not the plugin should be added to the default preset, if it's not present in any preset.</param>
/// <returns>Whether or not the plugin shall be enabled.</returns>
/// <param name="addIfNotDeclared">Whether the plugin should be added to the default preset, if it's not present in any preset.</param>
/// <returns>Whether the plugin shall be enabled.</returns>
public async Task<bool> GetWantStateAsync(Guid workingPluginId, string? internalName, bool defaultState, bool addIfNotDeclared = true)
{
var want = false;
@ -106,7 +106,7 @@ internal class ProfileManager : IServiceType
/// Check whether a plugin is declared in any profile.
/// </summary>
/// <param name="workingPluginId">The ID of the plugin.</param>
/// <returns>Whether or not the plugin is in any profile.</returns>
/// <returns>Whether the plugin is in any profile.</returns>
public bool IsInAnyProfile(Guid workingPluginId)
{
lock (this.profiles)
@ -118,7 +118,7 @@ internal class ProfileManager : IServiceType
/// A plugin can never be in the default profile if it is in any other profile.
/// </summary>
/// <param name="workingPluginId">The ID of the plugin.</param>
/// <returns>Whether or not the plugin is in the default profile.</returns>
/// <returns>Whether the plugin is in the default profile.</returns>
public bool IsInDefaultProfile(Guid workingPluginId)
=> this.DefaultProfile.WantsPlugin(workingPluginId) != null;
@ -193,6 +193,10 @@ internal class ProfileManager : IServiceType
}
}
}
else
{
throw new InvalidOperationException("Unsupported profile model version");
}
this.config.SavedProfiles!.Add(newModel);
this.config.QueueSave();

View file

@ -9,19 +9,47 @@ namespace Dalamud.Plugin.Internal.Profiles;
/// </summary>
public class ProfileModelV1 : ProfileModel
{
/// <summary>
/// Enum representing the startup policy of a profile.
/// </summary>
public enum ProfileStartupPolicy
{
/// <summary>
/// Remember the last state of the profile.
/// </summary>
RememberState,
/// <summary>
/// Always enable the profile.
/// </summary>
AlwaysEnable,
/// <summary>
/// Always disable the profile.
/// </summary>
AlwaysDisable,
}
/// <summary>
/// Gets the prefix of this version.
/// </summary>
public static string SerializedPrefix => "DP1";
/// <summary>
/// Gets or sets a value indicating whether or not this profile should always be enabled at boot.
/// Gets or sets a value indicating whether this profile should always be enabled at boot.
/// </summary>
[JsonProperty("b")]
[Obsolete("Superseded by StartupPolicy")]
public bool AlwaysEnableOnBoot { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not this profile is currently enabled.
/// Gets or sets the policy to use when Dalamud is loading.
/// </summary>
[JsonProperty("p")]
public ProfileStartupPolicy? StartupPolicy { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this profile is currently enabled.
/// </summary>
[JsonProperty("e")]
public bool IsEnabled { get; set; } = false;
@ -46,14 +74,14 @@ public class ProfileModelV1 : ProfileModel
/// Gets or sets the internal name of the plugin.
/// </summary>
public string? InternalName { get; set; }
/// <summary>
/// Gets or sets an ID uniquely identifying this specific instance of a plugin.
/// </summary>
public Guid WorkingPluginId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not this entry is enabled.
/// Gets or sets a value indicating whether this entry is enabled.
/// </summary>
public bool IsEnabled { get; set; }
}

View file

@ -10,7 +10,7 @@ internal class ProfilePluginEntry
/// </summary>
/// <param name="internalName">The internal name of the plugin.</param>
/// <param name="workingPluginId">The ID of the plugin.</param>
/// <param name="state">A value indicating whether or not this entry is enabled.</param>
/// <param name="state">A value indicating whether this entry is enabled.</param>
public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state)
{
this.InternalName = internalName;
@ -22,14 +22,14 @@ internal class ProfilePluginEntry
/// Gets the internal name of the plugin.
/// </summary>
public string InternalName { get; }
/// <summary>
/// Gets or sets an ID uniquely identifying this specific instance of a plugin.
/// </summary>
public Guid WorkingPluginId { get; set; }
/// <summary>
/// Gets a value indicating whether or not this entry is enabled.
/// Gets a value indicating whether this entry is enabled.
/// </summary>
public bool IsEnabled { get; }
}

View file

@ -15,7 +15,7 @@ namespace Dalamud.Plugin.Internal.Types;
/// This class represents a dev plugin and all facets of its lifecycle.
/// The DLL on disk, dependencies, loaded assembly, etc.
/// </summary>
internal class LocalDevPlugin : LocalPlugin
internal sealed class LocalDevPlugin : LocalPlugin
{
private static readonly ModuleLog Log = new("PLUGIN");
@ -41,7 +41,7 @@ internal class LocalDevPlugin : LocalPlugin
configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings();
configuration.QueueSave();
}
// Legacy dev plugins might not have this!
if (this.devSettings.WorkingPluginId == Guid.Empty)
{
@ -85,7 +85,17 @@ internal 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>
/// Gets an ID uniquely identifying this specific instance of a devPlugin.
/// </summary>
@ -152,7 +162,7 @@ internal class LocalDevPlugin : LocalPlugin
if (manifestPath.Exists)
this.manifest = LocalPluginManifest.Load(manifestPath) ?? throw new Exception("Could not reload manifest.");
}
/// <inheritdoc/>
protected override void OnPreReload()
{

View file

@ -15,6 +15,7 @@ using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Loader;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Utility;
namespace Dalamud.Plugin.Internal.Types;
@ -62,12 +63,13 @@ internal class LocalPlugin : IAsyncDisposable
}
this.DllFile = dllFile;
this.State = PluginState.Unloaded;
// Although it is conditionally used here, we need to set the initial value regardless.
this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
this.manifest = manifest;
this.State = PluginState.Unloaded;
var needsSaveDueToLegacyFiles = false;
// This converts from the ".disabled" file feature to the manifest instead.
@ -177,13 +179,13 @@ internal class LocalPlugin : IAsyncDisposable
public bool IsTesting => this.manifest.IsTestingExclusive || this.manifest.Testing;
/// <summary>
/// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not.
/// Gets a value indicating whether this plugin is orphaned(belongs to a repo) or not.
/// </summary>
public bool IsOrphaned => !this.IsDev &&
this.GetSourceRepository() == null;
/// <summary>
/// Gets a value indicating whether or not this plugin is serviced(repo still exists, but plugin no longer does).
/// Gets a value indicating whether this plugin is serviced(repo still exists, but plugin no longer does).
/// </summary>
public bool IsDecommissioned => !this.IsDev &&
this.GetSourceRepository()?.State == PluginRepositoryState.Success &&
@ -312,6 +314,9 @@ internal class LocalPlugin : IAsyncDisposable
if (!this.CheckPolicy())
throw new PluginPreconditionFailedException($"Unable to load {this.Name} as a load policy forbids it");
if (this.Manifest.MinimumDalamudVersion != null && this.Manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed)
throw new PluginPreconditionFailedException($"Unable to load {this.Name}, Dalamud version is lower than minimum required version {this.Manifest.MinimumDalamudVersion}");
this.State = PluginState.Loading;
Log.Information($"Loading {this.DllFile.Name}");
@ -352,19 +357,13 @@ internal class LocalPlugin : IAsyncDisposable
}
this.loader.Reload();
this.RefreshAssemblyInformation();
}
// Load the assembly
this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
this.AssemblyName = this.pluginAssembly.GetName();
// Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor.
this.pluginType ??= this.pluginAssembly.GetTypes()
.First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
Log.Verbose("{Name} ({Guid}): Have type", this.InternalName, this.EffectiveWorkingPluginId);
// Check for any loaded plugins with the same assembly name
var assemblyName = this.pluginAssembly.GetName().Name;
var assemblyName = this.pluginAssembly!.GetName().Name;
foreach (var otherPlugin in pluginManager.InstalledPlugins)
{
// During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed
@ -376,7 +375,7 @@ internal class LocalPlugin : IAsyncDisposable
if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null)
{
this.State = PluginState.Unloaded;
Log.Debug($"Duplicate assembly: {this.Name}");
Log.Debug("Duplicate assembly: {Name}", this.InternalName);
throw new DuplicatePluginException(assemblyName);
}
@ -392,7 +391,7 @@ internal class LocalPlugin : IAsyncDisposable
this.instance = await CreatePluginInstance(
this.manifest,
this.serviceScope,
this.pluginType,
this.pluginType!,
this.dalamudInterface);
this.State = PluginState.Loaded;
Log.Information("Finished loading {PluginName}", this.InternalName);
@ -504,7 +503,7 @@ internal class LocalPlugin : IAsyncDisposable
/// <summary>
/// Check if anything forbids this plugin from loading.
/// </summary>
/// <returns>Whether or not this plugin shouldn't load.</returns>
/// <returns>Whether this plugin shouldn't load.</returns>
public bool CheckPolicy()
{
var startInfo = Service<Dalamud>.Get().StartInfo;
@ -578,7 +577,7 @@ internal class LocalPlugin : IAsyncDisposable
var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create();
return await newInstanceTask.ConfigureAwait(false);
async Task<IDalamudPlugin> Create() => (IDalamudPlugin)await scope.CreateAsync(type, dalamudInterface);
async Task<IDalamudPlugin> Create() => (IDalamudPlugin)await scope.CreateAsync(type, ObjectInstanceVisibility.ExposedToPlugins, dalamudInterface);
}
private static void SetupLoaderConfig(LoaderConfig config)
@ -620,42 +619,60 @@ internal class LocalPlugin : IAsyncDisposable
throw;
}
this.RefreshAssemblyInformation();
}
private void RefreshAssemblyInformation()
{
if (this.loader == null)
throw new InvalidOperationException("No loader available");
try
{
this.pluginAssembly = this.loader.LoadDefaultAssembly();
this.AssemblyName = this.pluginAssembly.GetName();
}
catch (Exception ex)
{
this.pluginAssembly = null;
this.pluginType = null;
this.loader.Dispose();
this.ResetLoader();
Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}");
throw new InvalidPluginException(this.DllFile);
}
if (this.pluginAssembly == null)
{
this.ResetLoader();
Log.Error("Plugin assembly is null: {DllFileFullName}", this.DllFile.FullName);
throw new InvalidPluginException(this.DllFile);
}
try
{
this.pluginType = this.pluginAssembly.GetTypes().FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
}
catch (ReflectionTypeLoadException ex)
{
Log.Error(ex, $"Could not load one or more types when searching for IDalamudPlugin: {this.DllFile.FullName}");
// Something blew up when parsing types, but we still want to look for IDalamudPlugin. Let Load() handle the error.
this.pluginType = ex.Types.FirstOrDefault(type => type != null && type.IsAssignableTo(typeof(IDalamudPlugin)));
this.ResetLoader();
Log.Error(ex, "Could not load one or more types when searching for IDalamudPlugin: {DllFileFullName}", this.DllFile.FullName);
throw;
}
if (this.pluginType == default)
if (this.pluginType == null)
{
this.pluginAssembly = null;
this.pluginType = null;
this.loader.Dispose();
Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}");
this.ResetLoader();
Log.Error("Nothing inherits from IDalamudPlugin: {DllFileFullName}", this.DllFile.FullName);
throw new InvalidPluginException(this.DllFile);
}
}
private void ResetLoader()
{
this.pluginAssembly = null;
this.pluginType = null;
this.loader?.Dispose();
this.loader = null;
}
/// <summary>Clears and disposes all resources associated with the plugin instance.</summary>
/// <param name="disposalMode">Whether to clear and dispose <see cref="loader"/>.</param>
/// <returns>Exceptions, if any occurred.</returns>

View file

@ -16,7 +16,7 @@ public interface IPluginManifest
/// Gets the public name of the plugin.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a punchline of the plugins functions.
/// </summary>
@ -26,7 +26,7 @@ public interface IPluginManifest
/// Gets the author/s of the plugin.
/// </summary>
public string Author { get; }
/// <summary>
/// Gets a value indicating whether the plugin can be unloaded asynchronously.
/// </summary>
@ -41,17 +41,22 @@ public interface IPluginManifest
/// Gets the assembly version of the plugin's testing variant.
/// </summary>
public Version? TestingAssemblyVersion { get; }
/// <summary>
/// Gets the minimum Dalamud assembly version this plugin requires.
/// </summary>
public Version? MinimumDalamudVersion { get; }
/// <summary>
/// Gets the DIP17 channel name.
/// </summary>
public string? Dip17Channel { get; }
/// <summary>
/// Gets the last time this plugin was updated.
/// </summary>
public long LastUpdate { get; }
/// <summary>
/// Gets a changelog, null if none exists.
/// </summary>
@ -88,7 +93,7 @@ public interface IPluginManifest
/// Gets an URL to the website or source code of the plugin.
/// </summary>
public string? RepoUrl { get; }
/// <summary>
/// Gets a description of the plugins functions.
/// </summary>

View file

@ -42,7 +42,7 @@ internal record PluginManifest : IPluginManifest
public List<string>? CategoryTags { get; init; }
/// <summary>
/// Gets or sets a value indicating whether or not the plugin is hidden in the plugin installer.
/// Gets or sets a value indicating whether the plugin is hidden in the plugin installer.
/// This value comes from the plugin master and is in addition to the list of hidden names kept by Dalamud.
/// </summary>
[JsonProperty]
@ -75,6 +75,10 @@ internal record PluginManifest : IPluginManifest
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any;
/// <inheritdoc/>
[JsonProperty]
public Version? MinimumDalamudVersion { get; init; }
/// <inheritdoc/>
[JsonProperty]
public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel;