mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-23 08:17:47 +01:00
Merge branch 'master' into v9
This commit is contained in:
commit
2e0e46384c
70 changed files with 2694 additions and 623 deletions
|
|
@ -24,7 +24,9 @@ using Dalamud.IoC.Internal;
|
|||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Networking.Http;
|
||||
using Dalamud.Plugin.Internal.Exceptions;
|
||||
using Dalamud.Plugin.Internal.Profiles;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Ipc.Internal;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -44,6 +46,10 @@ namespace Dalamud.Plugin.Internal;
|
|||
// LocalPlugin uses ServiceContainer to create scopes
|
||||
[InherentDependency<ServiceContainer>]
|
||||
|
||||
// DalamudPluginInterface hands out a reference to this, so we have to keep it around
|
||||
// TODO api9: make it a service
|
||||
[InherentDependency<DataShare>]
|
||||
|
||||
#pragma warning restore SA1015
|
||||
internal partial class PluginManager : IDisposable, IServiceType
|
||||
{
|
||||
|
|
@ -62,7 +68,6 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
|
||||
private readonly object pluginListLock = new();
|
||||
private readonly DirectoryInfo pluginDirectory;
|
||||
private readonly DirectoryInfo devPluginDirectory;
|
||||
private readonly BannedPlugin[]? bannedPlugins;
|
||||
|
||||
private readonly DalamudLinkPayload openInstallerWindowPluginChangelogsLink;
|
||||
|
|
@ -73,6 +78,9 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudStartInfo startInfo = Service<DalamudStartInfo>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ProfileManager profileManager = Service<ProfileManager>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly HappyHttpClient happyHttpClient = Service<HappyHttpClient>.Get();
|
||||
|
||||
|
|
@ -409,7 +417,7 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
|
||||
try
|
||||
{
|
||||
pluginDefs.Add(versionsDefs.OrderByDescending(x => x.Manifest!.EffectiveVersion).First());
|
||||
pluginDefs.Add(versionsDefs.MaxBy(x => x.Manifest!.EffectiveVersion));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -797,122 +805,6 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
return plugin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a plugin.
|
||||
/// </summary>
|
||||
/// <param name="dllFile">The <see cref="FileInfo"/> associated with the main assembly of this plugin.</param>
|
||||
/// <param name="manifest">The already loaded definition, if available.</param>
|
||||
/// <param name="reason">The reason this plugin was loaded.</param>
|
||||
/// <param name="isDev">If this plugin should support development features.</param>
|
||||
/// <param name="isBoot">If this plugin is being loaded at boot.</param>
|
||||
/// <param name="doNotLoad">Don't load the plugin, just don't do it.</param>
|
||||
/// <returns>The loaded plugin.</returns>
|
||||
public async Task<LocalPlugin> LoadPluginAsync(FileInfo dllFile, LocalPluginManifest? manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false)
|
||||
{
|
||||
var name = manifest?.Name ?? dllFile.Name;
|
||||
var loadPlugin = !doNotLoad;
|
||||
|
||||
LocalPlugin plugin;
|
||||
|
||||
if (manifest != null && manifest.InternalName == null)
|
||||
{
|
||||
Log.Error("{FileName}: Your manifest has no internal name set! Can't load this.", dllFile.FullName);
|
||||
throw new Exception("No internal name");
|
||||
}
|
||||
|
||||
if (isDev)
|
||||
{
|
||||
Log.Information($"Loading dev plugin {name}");
|
||||
var devPlugin = new LocalDevPlugin(dllFile, manifest);
|
||||
loadPlugin &= !isBoot || devPlugin.StartOnBoot;
|
||||
|
||||
// If we're not loading it, make sure it's disabled
|
||||
if (!loadPlugin && !devPlugin.IsDisabled)
|
||||
devPlugin.Disable();
|
||||
|
||||
plugin = devPlugin;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information($"Loading plugin {name}");
|
||||
plugin = new LocalPlugin(dllFile, manifest);
|
||||
}
|
||||
|
||||
if (loadPlugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!plugin.IsDisabled && !plugin.IsOrphaned)
|
||||
{
|
||||
await plugin.LoadAsync(reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose($"{name} not loaded, disabled:{plugin.IsDisabled} orphaned:{plugin.IsOrphaned}");
|
||||
}
|
||||
}
|
||||
catch (InvalidPluginException)
|
||||
{
|
||||
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||
throw;
|
||||
}
|
||||
catch (BannedPluginException)
|
||||
{
|
||||
// Out of date plugins get added so they can be updated.
|
||||
Log.Information($"Plugin was banned, adding anyways: {dllFile.Name}");
|
||||
}
|
||||
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
|
||||
}
|
||||
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}");
|
||||
}
|
||||
else if (plugin.IsOrphaned)
|
||||
{
|
||||
// Orphaned plugins get added, so that users aren't confused.
|
||||
Log.Information(ex, $"Plugin was orphaned, adding anyways: {dllFile.Name}");
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (this.pluginListLock)
|
||||
{
|
||||
this.InstalledPlugins = this.InstalledPlugins.Add(plugin);
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a plugin.
|
||||
/// </summary>
|
||||
|
|
@ -1042,7 +934,7 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
if (plugin.InstalledPlugin.IsDev)
|
||||
continue;
|
||||
|
||||
if (plugin.InstalledPlugin.Manifest.Disabled && ignoreDisabled)
|
||||
if (!plugin.InstalledPlugin.IsWantedByAnyProfile && ignoreDisabled)
|
||||
continue;
|
||||
|
||||
if (plugin.InstalledPlugin.Manifest.ScheduledForDeletion)
|
||||
|
|
@ -1104,40 +996,28 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
|
||||
if (plugin.IsDev)
|
||||
{
|
||||
try
|
||||
{
|
||||
plugin.DllFile.Delete();
|
||||
lock (this.pluginListLock)
|
||||
{
|
||||
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error during delete (update)");
|
||||
updateStatus.WasUpdated = false;
|
||||
return updateStatus;
|
||||
}
|
||||
throw new Exception("We should never update a dev plugin");
|
||||
}
|
||||
else
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: Why were we ever doing this? We should never be loading the old version in the first place
|
||||
/*
|
||||
if (!plugin.IsDisabled)
|
||||
plugin.Disable();
|
||||
*/
|
||||
|
||||
lock (this.pluginListLock)
|
||||
{
|
||||
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
lock (this.pluginListLock)
|
||||
{
|
||||
Log.Error(ex, "Error during disable (update)");
|
||||
updateStatus.WasUpdated = false;
|
||||
return updateStatus;
|
||||
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error during disable (update)");
|
||||
updateStatus.WasUpdated = false;
|
||||
return updateStatus;
|
||||
}
|
||||
|
||||
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
|
||||
var dtr = Service<DtrBar>.Get();
|
||||
|
|
@ -1308,6 +1188,137 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
/// <returns>The calling plugin, or null.</returns>
|
||||
public LocalPlugin? FindCallingPlugin() => this.FindCallingPlugin(new StackTrace());
|
||||
|
||||
/// <summary>
|
||||
/// Load a plugin.
|
||||
/// </summary>
|
||||
/// <param name="dllFile">The <see cref="FileInfo"/> associated with the main assembly of this plugin.</param>
|
||||
/// <param name="manifest">The already loaded definition, if available.</param>
|
||||
/// <param name="reason">The reason this plugin was loaded.</param>
|
||||
/// <param name="isDev">If this plugin should support development features.</param>
|
||||
/// <param name="isBoot">If this plugin is being loaded at boot.</param>
|
||||
/// <param name="doNotLoad">Don't load the plugin, just don't do it.</param>
|
||||
/// <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)
|
||||
{
|
||||
var name = manifest?.Name ?? dllFile.Name;
|
||||
var loadPlugin = !doNotLoad;
|
||||
|
||||
LocalPlugin plugin;
|
||||
|
||||
if (manifest != null && manifest.InternalName == null)
|
||||
{
|
||||
Log.Error("{FileName}: Your manifest has no internal name set! Can't load this.", dllFile.FullName);
|
||||
throw new Exception("No internal name");
|
||||
}
|
||||
|
||||
if (isDev)
|
||||
{
|
||||
Log.Information($"Loading dev plugin {name}");
|
||||
var devPlugin = new LocalDevPlugin(dllFile, manifest);
|
||||
loadPlugin &= !isBoot || devPlugin.StartOnBoot;
|
||||
|
||||
var probablyInternalNameForThisPurpose = manifest?.InternalName ?? dllFile.Name;
|
||||
var wantsInDefaultProfile =
|
||||
this.profileManager.DefaultProfile.WantsPlugin(probablyInternalNameForThisPurpose);
|
||||
if (wantsInDefaultProfile == false && devPlugin.StartOnBoot)
|
||||
{
|
||||
this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, true, false);
|
||||
}
|
||||
else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot)
|
||||
{
|
||||
this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false);
|
||||
}
|
||||
|
||||
plugin = devPlugin;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information($"Loading plugin {name}");
|
||||
plugin = new LocalPlugin(dllFile, manifest);
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
var defaultState = manifest?.Disabled != true && loadPlugin;
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// Need to do this here, so plugins that don't load are still added to the default profile
|
||||
var wantToLoad = this.profileManager.GetWantState(plugin.Manifest.InternalName, defaultState);
|
||||
|
||||
if (loadPlugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (wantToLoad && !plugin.IsOrphaned)
|
||||
{
|
||||
await plugin.LoadAsync(reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose($"{name} not loaded, wantToLoad:{wantToLoad} orphaned:{plugin.IsOrphaned}");
|
||||
}
|
||||
}
|
||||
catch (InvalidPluginException)
|
||||
{
|
||||
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||
throw;
|
||||
}
|
||||
catch (BannedPluginException)
|
||||
{
|
||||
// Out of date plugins get added so they can be updated.
|
||||
Log.Information($"Plugin was banned, adding anyways: {dllFile.Name}");
|
||||
}
|
||||
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
|
||||
}
|
||||
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}");
|
||||
}
|
||||
else if (plugin.IsOrphaned)
|
||||
{
|
||||
// Orphaned plugins get added, so that users aren't confused.
|
||||
Log.Information(ex, $"Plugin was orphaned, adding anyways: {dllFile.Name}");
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (this.pluginListLock)
|
||||
{
|
||||
this.InstalledPlugins = this.InstalledPlugins.Add(plugin);
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
private void DetectAvailablePluginUpdates()
|
||||
{
|
||||
var updatablePlugins = new List<AvailablePluginUpdate>();
|
||||
|
|
|
|||
214
Dalamud/Plugin/Internal/Profiles/Profile.cs
Normal file
214
Dalamud/Plugin/Internal/Profiles/Profile.cs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a single runtime profile.
|
||||
/// </summary>
|
||||
internal class Profile
|
||||
{
|
||||
private static readonly ModuleLog Log = new("PROFILE");
|
||||
|
||||
private readonly ProfileManager manager;
|
||||
private readonly ProfileModelV1 modelV1;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Profile"/> class.
|
||||
/// </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>
|
||||
public Profile(ProfileManager manager, ProfileModel model, bool isDefaultProfile, bool isBoot)
|
||||
{
|
||||
this.manager = manager;
|
||||
this.IsDefaultProfile = isDefaultProfile;
|
||||
this.modelV1 = model as ProfileModelV1 ??
|
||||
throw new ArgumentException("Model was null or unhandled version");
|
||||
|
||||
// We don't actually enable plugins here, PM will do it on bootup
|
||||
if (isDefaultProfile)
|
||||
{
|
||||
// Default profile cannot be disabled
|
||||
this.IsEnabled = this.modelV1.IsEnabled = true;
|
||||
this.Name = this.modelV1.Name = "DEFAULT";
|
||||
}
|
||||
else if (this.modelV1.AlwaysEnableOnBoot && 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);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose("{Guid} not enabled", this.modelV1.Guid);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets this profile's name.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get => this.modelV1.Name;
|
||||
set
|
||||
{
|
||||
this.modelV1.Name = value;
|
||||
Service<DalamudConfiguration>.Get().QueueSave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this profile shall always be enabled at boot.
|
||||
/// </summary>
|
||||
public bool AlwaysEnableAtBoot
|
||||
{
|
||||
get => this.modelV1.AlwaysEnableOnBoot;
|
||||
set
|
||||
{
|
||||
this.modelV1.AlwaysEnableOnBoot = value;
|
||||
Service<DalamudConfiguration>.Get().QueueSave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets this profile's guid.
|
||||
/// </summary>
|
||||
public Guid Guid => this.modelV1.Guid;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not 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.
|
||||
/// </summary>
|
||||
public bool IsDefaultProfile { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all plugins declared in this profile.
|
||||
/// </summary>
|
||||
public IEnumerable<ProfilePluginEntry> Plugins =>
|
||||
this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.IsEnabled));
|
||||
|
||||
/// <summary>
|
||||
/// Gets this profile's underlying model.
|
||||
/// </summary>
|
||||
public ProfileModel Model => this.modelV1;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <exception cref="InvalidOperationException">Thrown when an untoggleable profile is toggled.</exception>
|
||||
public void SetState(bool enabled, bool apply = true)
|
||||
{
|
||||
if (this.IsDefaultProfile)
|
||||
throw new InvalidOperationException("Cannot set state of default profile");
|
||||
|
||||
Debug.Assert(this.IsEnabled != enabled, "Trying to set state of a profile to the same state");
|
||||
this.IsEnabled = this.modelV1.IsEnabled = enabled;
|
||||
Log.Verbose("Set state {State} for {Guid}", enabled, this.modelV1.Guid);
|
||||
|
||||
Service<DalamudConfiguration>.Get().QueueSave();
|
||||
|
||||
if (apply)
|
||||
this.manager.ApplyAllWantStates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if this profile contains a specific plugin, and if it is enabled.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <returns>Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled.</returns>
|
||||
public bool? WantsPlugin(string internalName)
|
||||
{
|
||||
var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
|
||||
return entry?.IsEnabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a plugin to this profile with the desired state, or change the state of a plugin in this profile.
|
||||
/// This will block until all states have been applied.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</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>
|
||||
public void AddOrUpdate(string internalName, bool state, bool apply = true)
|
||||
{
|
||||
Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()");
|
||||
|
||||
var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.IsEnabled = state;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin
|
||||
{
|
||||
InternalName = internalName,
|
||||
IsEnabled = state,
|
||||
});
|
||||
}
|
||||
|
||||
// We need to remove this plugin from the default profile, if it declares it.
|
||||
if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(internalName) != null)
|
||||
{
|
||||
this.manager.DefaultProfile.Remove(internalName, false);
|
||||
}
|
||||
|
||||
Service<DalamudConfiguration>.Get().QueueSave();
|
||||
|
||||
if (apply)
|
||||
this.manager.ApplyAllWantStates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a plugin from this profile.
|
||||
/// This will block until all states have been applied.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
|
||||
public void Remove(string internalName, bool apply = true)
|
||||
{
|
||||
var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
|
||||
if (entry == null)
|
||||
throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\"");
|
||||
|
||||
if (!this.modelV1.Plugins.Remove(entry))
|
||||
throw new Exception("Couldn't remove plugin from model collection");
|
||||
|
||||
// We need to add this plugin back to the default profile, if we were the last profile to have it.
|
||||
if (!this.manager.IsInAnyProfile(internalName))
|
||||
{
|
||||
if (!this.IsDefaultProfile)
|
||||
{
|
||||
this.manager.DefaultProfile.AddOrUpdate(internalName, entry.IsEnabled, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Removed plugin from default profile, but wasn't in any other profile");
|
||||
}
|
||||
}
|
||||
|
||||
Service<DalamudConfiguration>.Get().QueueSave();
|
||||
|
||||
if (apply)
|
||||
this.manager.ApplyAllWantStates();
|
||||
}
|
||||
}
|
||||
176
Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs
Normal file
176
Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using CheapLoc;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Utility;
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for profile-related chat commands.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class ProfileCommandHandler : IServiceType, IDisposable
|
||||
{
|
||||
private readonly CommandManager cmd;
|
||||
private readonly ProfileManager profileManager;
|
||||
private readonly ChatGui chat;
|
||||
private readonly Framework framework;
|
||||
|
||||
private List<(string, ProfileOp)> queue = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProfileCommandHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="cmd">Command handler.</param>
|
||||
/// <param name="profileManager">Profile manager.</param>
|
||||
/// <param name="chat">Chat handler.</param>
|
||||
/// <param name="framework">Framework.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public ProfileCommandHandler(CommandManager cmd, ProfileManager profileManager, ChatGui chat, Framework framework)
|
||||
{
|
||||
this.cmd = cmd;
|
||||
this.profileManager = profileManager;
|
||||
this.chat = chat;
|
||||
this.framework = framework;
|
||||
|
||||
this.cmd.AddHandler("/xlenableprofile", new CommandInfo(this.OnEnableProfile)
|
||||
{
|
||||
HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler("/xldisableprofile", new CommandInfo(this.OnDisableProfile)
|
||||
{
|
||||
HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler("/xltoggleprofile", new CommandInfo(this.OnToggleProfile)
|
||||
{
|
||||
HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.framework.Update += this.FrameworkOnUpdate;
|
||||
}
|
||||
|
||||
private enum ProfileOp
|
||||
{
|
||||
Enable,
|
||||
Disable,
|
||||
Toggle,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.cmd.RemoveHandler("/xlenablecollection");
|
||||
this.cmd.RemoveHandler("/xldisablecollection");
|
||||
this.cmd.RemoveHandler("/xltogglecollection");
|
||||
|
||||
this.framework.Update += this.FrameworkOnUpdate;
|
||||
}
|
||||
|
||||
private void FrameworkOnUpdate(Framework framework1)
|
||||
{
|
||||
if (this.profileManager.IsBusy)
|
||||
return;
|
||||
|
||||
if (this.queue.Count > 0)
|
||||
{
|
||||
var op = this.queue[0];
|
||||
this.queue.RemoveAt(0);
|
||||
|
||||
var profile = this.profileManager.Profiles.FirstOrDefault(x => x.Name == op.Item1);
|
||||
if (profile == null || profile.IsDefaultProfile)
|
||||
return;
|
||||
|
||||
switch (op.Item2)
|
||||
{
|
||||
case ProfileOp.Enable:
|
||||
if (!profile.IsEnabled)
|
||||
profile.SetState(true, false);
|
||||
break;
|
||||
case ProfileOp.Disable:
|
||||
if (profile.IsEnabled)
|
||||
profile.SetState(false, false);
|
||||
break;
|
||||
case ProfileOp.Toggle:
|
||||
profile.SetState(!profile.IsEnabled, false);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
if (profile.IsEnabled)
|
||||
{
|
||||
this.chat.Print(Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name));
|
||||
}
|
||||
|
||||
Task.Run(() => this.profileManager.ApplyAllWantStates()).ContinueWith(t =>
|
||||
{
|
||||
if (!t.IsCompletedSuccessfully && t.Exception != null)
|
||||
{
|
||||
Log.Error(t.Exception, "Could not apply profiles through commands");
|
||||
this.chat.PrintError(Loc.Localize("ProfileCommandsApplyFailed", "Failed to apply your collections. Please check the console for errors."));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.chat.Print(Loc.Localize("ProfileCommandsApplySuccess", "Collections applied."));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnableProfile(string command, string arguments)
|
||||
{
|
||||
var name = this.ValidateName(arguments);
|
||||
if (name == null)
|
||||
return;
|
||||
|
||||
this.queue = this.queue.Where(x => x.Item1 != name).ToList();
|
||||
this.queue.Add((name, ProfileOp.Enable));
|
||||
}
|
||||
|
||||
private void OnDisableProfile(string command, string arguments)
|
||||
{
|
||||
var name = this.ValidateName(arguments);
|
||||
if (name == null)
|
||||
return;
|
||||
|
||||
this.queue = this.queue.Where(x => x.Item1 != name).ToList();
|
||||
this.queue.Add((name, ProfileOp.Disable));
|
||||
}
|
||||
|
||||
private void OnToggleProfile(string command, string arguments)
|
||||
{
|
||||
var name = this.ValidateName(arguments);
|
||||
if (name == null)
|
||||
return;
|
||||
|
||||
this.queue.Add((name, ProfileOp.Toggle));
|
||||
}
|
||||
|
||||
private string? ValidateName(string arguments)
|
||||
{
|
||||
var name = arguments.Replace("\"", string.Empty);
|
||||
if (this.profileManager.Profiles.All(x => x.Name != name))
|
||||
{
|
||||
this.chat.PrintError($"No collection like \"{name}\".");
|
||||
return null;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
282
Dalamud/Plugin/Internal/Profiles/ProfileManager.cs
Normal file
282
Dalamud/Plugin/Internal/Profiles/ProfileManager.cs
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using CheapLoc;
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for managing plugin profiles.
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal class ProfileManager : IServiceType
|
||||
{
|
||||
private static readonly ModuleLog Log = new("PROFMAN");
|
||||
private readonly DalamudConfiguration config;
|
||||
|
||||
private readonly List<Profile> profiles = new();
|
||||
|
||||
private volatile bool isBusy = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProfileManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">Dalamud config.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public ProfileManager(DalamudConfiguration config)
|
||||
{
|
||||
this.config = config;
|
||||
|
||||
this.LoadProfilesFromConfigInitially();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default profile.
|
||||
/// </summary>
|
||||
public Profile DefaultProfile => this.profiles.First(x => x.IsDefaultProfile);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all profiles, including the default profile.
|
||||
/// </summary>
|
||||
public IEnumerable<Profile> Profiles => this.profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the profile manager is busy enabling/disabling plugins.
|
||||
/// </summary>
|
||||
public bool IsBusy => this.isBusy;
|
||||
|
||||
/// <summary>
|
||||
/// Check if any enabled profile wants a specific plugin enabled.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</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>
|
||||
public bool GetWantState(string internalName, bool defaultState, bool addIfNotDeclared = true)
|
||||
{
|
||||
var want = false;
|
||||
var wasInAnyProfile = false;
|
||||
|
||||
foreach (var profile in this.profiles)
|
||||
{
|
||||
var state = profile.WantsPlugin(internalName);
|
||||
if (state.HasValue)
|
||||
{
|
||||
want = want || (profile.IsEnabled && state.Value);
|
||||
wasInAnyProfile = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasInAnyProfile && addIfNotDeclared)
|
||||
{
|
||||
Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState);
|
||||
this.DefaultProfile.AddOrUpdate(internalName, defaultState, false);
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
return want;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a plugin is declared in any profile.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <returns>Whether or not the plugin is in any profile.</returns>
|
||||
public bool IsInAnyProfile(string internalName)
|
||||
=> this.profiles.Any(x => x.WantsPlugin(internalName) != null);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a plugin is only in the default profile.
|
||||
/// A plugin can never be in the default profile if it is in any other profile.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <returns>Whether or not the plugin is in the default profile.</returns>
|
||||
public bool IsInDefaultProfile(string internalName)
|
||||
=> this.DefaultProfile.WantsPlugin(internalName) != null;
|
||||
|
||||
/// <summary>
|
||||
/// Add a new profile.
|
||||
/// </summary>
|
||||
/// <returns>The added profile.</returns>
|
||||
public Profile AddNewProfile()
|
||||
{
|
||||
var model = new ProfileModelV1
|
||||
{
|
||||
Guid = Guid.NewGuid(),
|
||||
Name = this.GenerateUniqueProfileName(Loc.Localize("PluginProfilesNewProfile", "New Collection")),
|
||||
IsEnabled = false,
|
||||
};
|
||||
|
||||
this.config.SavedProfiles!.Add(model);
|
||||
this.config.QueueSave();
|
||||
|
||||
var profile = new Profile(this, model, false, false);
|
||||
this.profiles.Add(profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone a specified profile.
|
||||
/// </summary>
|
||||
/// <param name="toClone">The profile to clone.</param>
|
||||
/// <returns>The newly cloned profile.</returns>
|
||||
public Profile CloneProfile(Profile toClone)
|
||||
{
|
||||
var newProfile = this.ImportProfile(toClone.Model.Serialize());
|
||||
if (newProfile == null)
|
||||
throw new Exception("New profile was null while cloning");
|
||||
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import a profile with a sharing string.
|
||||
/// </summary>
|
||||
/// <param name="data">The sharing string to import.</param>
|
||||
/// <returns>The imported profile, or null, if the string was invalid.</returns>
|
||||
public Profile? ImportProfile(string data)
|
||||
{
|
||||
var newModel = ProfileModel.Deserialize(data);
|
||||
if (newModel == null)
|
||||
return null;
|
||||
|
||||
newModel.Guid = Guid.NewGuid();
|
||||
newModel.Name = this.GenerateUniqueProfileName(newModel.Name.IsNullOrEmpty() ? "Unknown Collection" : newModel.Name);
|
||||
if (newModel is ProfileModelV1 modelV1)
|
||||
modelV1.IsEnabled = false;
|
||||
|
||||
this.config.SavedProfiles!.Add(newModel);
|
||||
this.config.QueueSave();
|
||||
|
||||
var profile = new Profile(this, newModel, false, false);
|
||||
this.profiles.Add(profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go through all profiles and plugins, and enable/disable plugins they want active.
|
||||
/// This will block until all plugins have been loaded/reloaded.
|
||||
/// </summary>
|
||||
public void ApplyAllWantStates()
|
||||
{
|
||||
this.isBusy = true;
|
||||
Log.Information("Getting want states...");
|
||||
|
||||
var wantActive = this.profiles
|
||||
.Where(x => x.IsEnabled)
|
||||
.SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled)
|
||||
.Select(plugin => plugin.InternalName))
|
||||
.Distinct().ToList();
|
||||
|
||||
foreach (var internalName in wantActive)
|
||||
{
|
||||
Log.Information("\t=> Want {Name}", internalName);
|
||||
}
|
||||
|
||||
Log.Information("Applying want states...");
|
||||
|
||||
var pm = Service<PluginManager>.Get();
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var installedPlugin in pm.InstalledPlugins)
|
||||
{
|
||||
var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName);
|
||||
switch (wantThis)
|
||||
{
|
||||
case true when !installedPlugin.IsLoaded:
|
||||
if (installedPlugin.ApplicableForLoad)
|
||||
{
|
||||
Log.Information("\t=> Enabling {Name}", installedPlugin.Manifest.InternalName);
|
||||
tasks.Add(installedPlugin.LoadAsync(PluginLoadReason.Installer));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("\t=> {Name} wanted active, but not applicable", installedPlugin.Manifest.InternalName);
|
||||
}
|
||||
|
||||
break;
|
||||
case false when installedPlugin.IsLoaded:
|
||||
Log.Information("\t=> Disabling {Name}", installedPlugin.Manifest.InternalName);
|
||||
tasks.Add(installedPlugin.UnloadAsync());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This is probably not ideal... Might need to rethink the error handling strategy for this.
|
||||
try
|
||||
{
|
||||
Task.WaitAll(tasks.ToArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Couldn't apply state for one or more plugins");
|
||||
}
|
||||
|
||||
Log.Information("Applied!");
|
||||
this.isBusy = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a profile.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You should definitely apply states after this. It doesn't do it for you.
|
||||
/// </remarks>
|
||||
/// <param name="profile">The profile to delete.</param>
|
||||
public void DeleteProfile(Profile profile)
|
||||
{
|
||||
// We need to remove all plugins from the profile first, so that they are re-added to the default profile if needed
|
||||
foreach (var plugin in profile.Plugins.ToArray())
|
||||
{
|
||||
profile.Remove(plugin.InternalName, false);
|
||||
}
|
||||
|
||||
if (!this.config.SavedProfiles!.Remove(profile.Model))
|
||||
throw new Exception("Couldn't remove profile from models");
|
||||
|
||||
if (!this.profiles.Remove(profile))
|
||||
throw new Exception("Couldn't remove runtime profile");
|
||||
|
||||
this.config.QueueSave();
|
||||
}
|
||||
|
||||
private string GenerateUniqueProfileName(string startingWith)
|
||||
{
|
||||
if (this.profiles.All(x => x.Name != startingWith))
|
||||
return startingWith;
|
||||
|
||||
startingWith = Regex.Replace(startingWith, @" \(.* Mix\)", string.Empty);
|
||||
|
||||
while (true)
|
||||
{
|
||||
var newName = $"{startingWith} ({CultureInfo.InvariantCulture.TextInfo.ToTitleCase(Util.GetRandomName())} Mix)";
|
||||
|
||||
if (this.profiles.All(x => x.Name != newName))
|
||||
return newName;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadProfilesFromConfigInitially()
|
||||
{
|
||||
this.config.DefaultProfile ??= new ProfileModelV1();
|
||||
this.profiles.Add(new Profile(this, this.config.DefaultProfile, true, true));
|
||||
|
||||
this.config.SavedProfiles ??= new List<ProfileModel>();
|
||||
foreach (var profileModel in this.config.SavedProfiles)
|
||||
{
|
||||
this.profiles.Add(new Profile(this, profileModel, false, true));
|
||||
}
|
||||
|
||||
this.config.QueueSave();
|
||||
}
|
||||
}
|
||||
60
Dalamud/Plugin/Internal/Profiles/ProfileModel.cs
Normal file
60
Dalamud/Plugin/Internal/Profiles/ProfileModel.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
|
||||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a profile.
|
||||
/// </summary>
|
||||
public abstract class ProfileModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the profile.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public Guid Guid { get; set; } = Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the profile.
|
||||
/// </summary>
|
||||
[JsonProperty("n")]
|
||||
public string Name { get; set; } = "New Collection";
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a profile into a model.
|
||||
/// </summary>
|
||||
/// <param name="model">The string to decompress.</param>
|
||||
/// <returns>The parsed model.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the parsed string is not a valid profile.</exception>
|
||||
public static ProfileModel? Deserialize(string model)
|
||||
{
|
||||
var json = Util.DecompressString(Convert.FromBase64String(model.Substring(3)));
|
||||
|
||||
if (model.StartsWith(ProfileModelV1.SerializedPrefix))
|
||||
return JsonConvert.DeserializeObject<ProfileModelV1>(json);
|
||||
|
||||
throw new ArgumentException("Was not a compressed profile.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize this model into a string usable for sharing.
|
||||
/// </summary>
|
||||
/// <returns>The serialized representation of the model.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when an unsupported model is serialized.</exception>
|
||||
public string Serialize()
|
||||
{
|
||||
string prefix;
|
||||
switch (this)
|
||||
{
|
||||
case ProfileModelV1:
|
||||
prefix = ProfileModelV1.SerializedPrefix;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return prefix + Convert.ToBase64String(Util.CompressString(JsonConvert.SerializeObject(this)));
|
||||
}
|
||||
}
|
||||
55
Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs
Normal file
55
Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Version 1 of the profile model.
|
||||
/// </summary>
|
||||
public class ProfileModelV1 : ProfileModel
|
||||
{
|
||||
/// <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.
|
||||
/// </summary>
|
||||
[JsonProperty("b")]
|
||||
public bool AlwaysEnableOnBoot { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not this profile is currently enabled.
|
||||
/// </summary>
|
||||
[JsonProperty("e")]
|
||||
public bool IsEnabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating this profile's color.
|
||||
/// </summary>
|
||||
[JsonProperty("c")]
|
||||
public uint Color { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of plugins in this profile.
|
||||
/// </summary>
|
||||
public List<ProfileModelV1Plugin> Plugins { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a single plugin in a profile.
|
||||
/// </summary>
|
||||
public class ProfileModelV1Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the internal name of the plugin.
|
||||
/// </summary>
|
||||
public string? InternalName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not this entry is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; }
|
||||
}
|
||||
}
|
||||
28
Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs
Normal file
28
Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
namespace Dalamud.Plugin.Internal.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a single plugin in a profile.
|
||||
/// </summary>
|
||||
internal class ProfilePluginEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProfilePluginEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <param name="state">A value indicating whether or not this entry is enabled.</param>
|
||||
public ProfilePluginEntry(string internalName, bool state)
|
||||
{
|
||||
this.InternalName = internalName;
|
||||
this.IsEnabled = state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the internal name of the plugin.
|
||||
/// </summary>
|
||||
public string InternalName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this entry is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; }
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ using Dalamud.Logging;
|
|||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Internal.Exceptions;
|
||||
using Dalamud.Plugin.Internal.Loader;
|
||||
using Dalamud.Plugin.Internal.Profiles;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
|
@ -135,7 +136,9 @@ internal class LocalPlugin : IDisposable
|
|||
this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
|
||||
if (this.disabledFile.Exists)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
this.Manifest.Disabled = true;
|
||||
#pragma warning restore CS0618
|
||||
this.disabledFile.Delete();
|
||||
}
|
||||
|
||||
|
|
@ -206,9 +209,11 @@ internal class LocalPlugin : IDisposable
|
|||
public bool IsLoaded => this.State == PluginState.Loaded;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is disabled.
|
||||
/// Gets a value indicating whether this plugin is wanted active by any profile.
|
||||
/// INCLUDES the default profile.
|
||||
/// </summary>
|
||||
public bool IsDisabled => this.Manifest.Disabled;
|
||||
public bool IsWantedByAnyProfile =>
|
||||
Service<ProfileManager>.Get().GetWantState(this.Manifest.InternalName, false, false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin's API level is out of date.
|
||||
|
|
@ -244,6 +249,12 @@ internal class LocalPlugin : IDisposable
|
|||
/// </summary>
|
||||
public bool IsDev => this is LocalDevPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin should be allowed to load.
|
||||
/// </summary>
|
||||
public bool ApplicableForLoad => !this.IsBanned && !this.IsDecommissioned && !this.IsOrphaned && !this.IsOutdated
|
||||
&& !(!this.IsDev && this.State == PluginState.UnloadError) && this.CheckPolicy();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service scope for this plugin.
|
||||
/// </summary>
|
||||
|
|
@ -289,7 +300,6 @@ internal class LocalPlugin : IDisposable
|
|||
/// <returns>A task.</returns>
|
||||
public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
|
||||
{
|
||||
var configuration = await Service<DalamudConfiguration>.GetAsync();
|
||||
var framework = await Service<Framework>.GetAsync();
|
||||
var ioc = await Service<ServiceContainer>.GetAsync();
|
||||
var pluginManager = await Service<PluginManager>.GetAsync();
|
||||
|
|
@ -311,6 +321,10 @@ internal class LocalPlugin : IDisposable
|
|||
this.ReloadManifest();
|
||||
}
|
||||
|
||||
// If we reload a plugin we don't want to delete it. Makes sense, right?
|
||||
this.Manifest.ScheduledForDeletion = false;
|
||||
this.SaveManifest();
|
||||
|
||||
switch (this.State)
|
||||
{
|
||||
case PluginState.Loaded:
|
||||
|
|
@ -348,8 +362,9 @@ internal class LocalPlugin : IDisposable
|
|||
if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels)
|
||||
throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
|
||||
|
||||
if (this.Manifest.Disabled)
|
||||
throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled");
|
||||
// We might want to throw here?
|
||||
if (!this.IsWantedByAnyProfile)
|
||||
Log.Warning("{Name} is loading, but isn't wanted by any profile", this.Name);
|
||||
|
||||
if (this.IsOrphaned)
|
||||
throw new InvalidPluginOperationException($"Plugin {this.Name} had no associated repo.");
|
||||
|
|
@ -575,42 +590,6 @@ internal class LocalPlugin : IDisposable
|
|||
await this.LoadAsync(PluginLoadReason.Reload, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revert a disable. Must be unloaded first, does not load.
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
// Allowed: Unloaded, UnloadError
|
||||
switch (this.State)
|
||||
{
|
||||
case PluginState.Loading:
|
||||
case PluginState.Unloading:
|
||||
case PluginState.Loaded:
|
||||
case PluginState.LoadError:
|
||||
if (!this.IsDev)
|
||||
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
|
||||
break;
|
||||
case PluginState.Unloaded:
|
||||
break;
|
||||
case PluginState.UnloadError:
|
||||
break;
|
||||
case PluginState.DependencyResolutionFailed:
|
||||
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, dependency resolution failed");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(this.State.ToString());
|
||||
}
|
||||
|
||||
// NOTE(goat): This is inconsequential, and we do have situations where a plugin can end up enabled but not loaded:
|
||||
// Orphaned plugins can have their repo added back, but may not have been loaded at boot and may still be enabled.
|
||||
// We don't want to disable orphaned plugins when they are orphaned so this is how it's going to be.
|
||||
// if (!this.Manifest.Disabled)
|
||||
// throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
|
||||
|
||||
this.Manifest.Disabled = false;
|
||||
this.Manifest.ScheduledForDeletion = false;
|
||||
this.SaveManifest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if anything forbids this plugin from loading.
|
||||
/// </summary>
|
||||
|
|
@ -632,36 +611,6 @@ internal class LocalPlugin : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable this plugin, must be unloaded first.
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
// Allowed: Unloaded, UnloadError
|
||||
switch (this.State)
|
||||
{
|
||||
case PluginState.Loading:
|
||||
case PluginState.Unloading:
|
||||
case PluginState.Loaded:
|
||||
case PluginState.LoadError:
|
||||
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");
|
||||
case PluginState.Unloaded:
|
||||
break;
|
||||
case PluginState.UnloadError:
|
||||
break;
|
||||
case PluginState.DependencyResolutionFailed:
|
||||
return; // This is a no-op.
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(this.State.ToString());
|
||||
}
|
||||
|
||||
if (this.Manifest.Disabled)
|
||||
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, already disabled");
|
||||
|
||||
this.Manifest.Disabled = true;
|
||||
this.SaveManifest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedule the deletion of this plugin on next cleanup.
|
||||
/// </summary>
|
||||
|
|
@ -680,9 +629,9 @@ internal class LocalPlugin : IDisposable
|
|||
var manifest = LocalPluginManifest.GetManifestFile(this.DllFile);
|
||||
if (manifest.Exists)
|
||||
{
|
||||
var isDisabled = this.IsDisabled; // saving the internal state because it could have been deleted
|
||||
// var isDisabled = this.IsDisabled; // saving the internal state because it could have been deleted
|
||||
this.Manifest = LocalPluginManifest.Load(manifest);
|
||||
this.Manifest.Disabled = isDisabled;
|
||||
// this.Manifest.Disabled = isDisabled;
|
||||
|
||||
this.SaveManifest();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ internal record LocalPluginManifest : PluginManifest
|
|||
/// 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.
|
||||
/// </summary>
|
||||
[Obsolete("This is merely used for migrations now. Use the profile manager to check if a plugin shall be enabled.")]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -162,6 +162,12 @@ internal record PluginManifest
|
|||
[JsonProperty]
|
||||
public bool CanUnloadAsync { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin supports profiles.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public bool SupportsProfiles { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of screenshot image URLs to show in the plugin installer.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
|||
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Networking.Http;
|
||||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
|
@ -145,7 +146,7 @@ internal class PluginRepository
|
|||
return;
|
||||
}
|
||||
|
||||
this.PluginMaster = pluginMaster.AsReadOnly();
|
||||
this.PluginMaster = pluginMaster.Where(this.IsValidManifest).ToList().AsReadOnly();
|
||||
|
||||
Log.Information($"Successfully fetched repo: {this.PluginMasterUrl}");
|
||||
this.State = PluginRepositoryState.Success;
|
||||
|
|
@ -156,4 +157,28 @@ internal class PluginRepository
|
|||
this.State = PluginRepositoryState.Fail;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidManifest(RemotePluginManifest manifest)
|
||||
{
|
||||
if (manifest.InternalName.IsNullOrWhitespace())
|
||||
{
|
||||
Log.Error("Repository at {RepoLink} has a plugin with an invalid InternalName.", this.PluginMasterUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (manifest.Name.IsNullOrWhitespace())
|
||||
{
|
||||
Log.Error("Plugin {PluginName} in {RepoLink} has an invalid Name.", manifest.InternalName, this.PluginMasterUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||
if (manifest.AssemblyVersion == null)
|
||||
{
|
||||
Log.Error("Plugin {PluginName} in {RepoLink} has an invalid AssemblyVersion.", manifest.InternalName, this.PluginMasterUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue