diff --git a/.editorconfig b/.editorconfig
index 109c9f406..1e4724862 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -123,10 +123,12 @@ resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_
resharper_invert_if_highlighting = none
resharper_loop_can_be_converted_to_query_highlighting = none
resharper_method_has_async_overload_highlighting = none
+resharper_private_field_can_be_converted_to_local_variable_highlighting = none
resharper_redundant_base_qualifier_highlighting = none
resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint
+resharper_unused_auto_property_accessor_global_highlighting = none
csharp_style_deconstructed_variable_declaration=true:silent
[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}]
diff --git a/Dalamud.sln.DotSettings b/Dalamud.sln.DotSettings
index 690f1a7ac..188e70c2b 100644
--- a/Dalamud.sln.DotSettings
+++ b/Dalamud.sln.DotSettings
@@ -50,11 +50,15 @@
True
True
True
+ True
True
True
+ True
True
True
True
+ True
+ True
True
True
True
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs
index ce2353ec8..3e955e389 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs
@@ -1,6 +1,7 @@
using System;
using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
namespace Dalamud.Interface.Internal.Windows.PluginInstaller
diff --git a/Dalamud/Plugin/Internal/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/LocalDevPlugin.cs
deleted file mode 100644
index 27989531c..000000000
--- a/Dalamud/Plugin/Internal/LocalDevPlugin.cs
+++ /dev/null
@@ -1,164 +0,0 @@
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-using Dalamud.Configuration.Internal;
-using Dalamud.Interface.Internal.Notifications;
-using Dalamud.Logging.Internal;
-using Dalamud.Plugin.Internal.Types;
-
-namespace Dalamud.Plugin.Internal
-{
- ///
- /// This class represents a dev plugin and all facets of its lifecycle.
- /// The DLL on disk, dependencies, loaded assembly, etc.
- ///
- internal class LocalDevPlugin : LocalPlugin, IDisposable
- {
- private static readonly ModuleLog Log = new("PLUGIN");
-
- // Ref to Dalamud.Configuration.DevPluginSettings
- private readonly DevPluginSettings devSettings;
-
- private FileSystemWatcher? fileWatcher;
- private CancellationTokenSource fileWatcherTokenSource = new();
- private int reloadCounter;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Path to the DLL file.
- /// The plugin manifest.
- public LocalDevPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
- : base(dllFile, manifest)
- {
- var configuration = Service.Get();
-
- if (!configuration.DevPluginSettings.TryGetValue(dllFile.FullName, out this.devSettings))
- {
- configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings();
- configuration.Save();
- }
-
- if (this.AutomaticReload)
- {
- this.EnableReloading();
- }
- }
-
- ///
- /// Gets or sets a value indicating whether this dev plugin should start on boot.
- ///
- public bool StartOnBoot
- {
- get => this.devSettings.StartOnBoot;
- set => this.devSettings.StartOnBoot = value;
- }
-
- ///
- /// Gets or sets a value indicating whether this dev plugin should reload on change.
- ///
- public bool AutomaticReload
- {
- get => this.devSettings.AutomaticReloading;
- set
- {
- this.devSettings.AutomaticReloading = value;
-
- if (this.devSettings.AutomaticReloading)
- {
- this.EnableReloading();
- }
- else
- {
- this.DisableReloading();
- }
- }
- }
-
- ///
- public new void Dispose()
- {
- if (this.fileWatcher != null)
- {
- this.fileWatcher.Changed -= this.OnFileChanged;
- this.fileWatcherTokenSource.Cancel();
- this.fileWatcher.Dispose();
- }
-
- base.Dispose();
- }
-
- ///
- /// Configure this plugin for automatic reloading and enable it.
- ///
- public void EnableReloading()
- {
- if (this.fileWatcher == null)
- {
- this.fileWatcherTokenSource = new();
- this.fileWatcher = new FileSystemWatcher(this.DllFile.DirectoryName);
- this.fileWatcher.Changed += this.OnFileChanged;
- this.fileWatcher.Filter = this.DllFile.Name;
- this.fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
- this.fileWatcher.EnableRaisingEvents = true;
- }
- }
-
- ///
- /// Disable automatic reloading for this plugin.
- ///
- public void DisableReloading()
- {
- if (this.fileWatcher != null)
- {
- this.fileWatcherTokenSource.Cancel();
- this.fileWatcher.Changed -= this.OnFileChanged;
- this.fileWatcher.Dispose();
- this.fileWatcher = null;
- }
- }
-
- private void OnFileChanged(object sender, FileSystemEventArgs args)
- {
- var current = Interlocked.Increment(ref this.reloadCounter);
-
- Task.Delay(500).ContinueWith(
- _ =>
- {
- if (this.fileWatcherTokenSource.IsCancellationRequested)
- {
- Log.Debug($"Skipping reload of {this.Name}, file watcher was cancelled.");
- return;
- }
-
- if (current != this.reloadCounter)
- {
- Log.Debug($"Skipping reload of {this.Name}, file has changed again.");
- return;
- }
-
- if (this.State != PluginState.Loaded)
- {
- Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded}.");
- return;
- }
-
- var notificationManager = Service.Get();
-
- try
- {
- this.Reload();
- notificationManager.AddNotification($"The DevPlugin '{this.Name} was reloaded successfully.", "Plugin reloaded!", NotificationType.Success);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "DevPlugin reload failed.");
- notificationManager.AddNotification($"The DevPlugin '{this.Name} could not be reloaded.", "Plugin reload failed!", NotificationType.Error);
- }
- },
- this.fileWatcherTokenSource.Token);
- }
- }
-}
diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs
deleted file mode 100644
index ba6297103..000000000
--- a/Dalamud/Plugin/Internal/LocalPlugin.cs
+++ /dev/null
@@ -1,481 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-
-using Dalamud.Configuration.Internal;
-using Dalamud.Game;
-using Dalamud.Game.Gui.Dtr;
-using Dalamud.IoC.Internal;
-using Dalamud.Logging.Internal;
-using Dalamud.Plugin.Internal.Exceptions;
-using Dalamud.Plugin.Internal.Loader;
-using Dalamud.Plugin.Internal.Types;
-using Dalamud.Utility;
-using Dalamud.Utility.Signatures;
-
-namespace Dalamud.Plugin.Internal
-{
- ///
- /// This class represents a plugin and all facets of its lifecycle.
- /// The DLL on disk, dependencies, loaded assembly, etc.
- ///
- internal class LocalPlugin : IDisposable
- {
- private static readonly ModuleLog Log = new("LOCALPLUGIN");
-
- private readonly FileInfo manifestFile;
- private readonly FileInfo disabledFile;
- private readonly FileInfo testingFile;
-
- private PluginLoader? loader;
- private Assembly? pluginAssembly;
- private Type? pluginType;
- private IDalamudPlugin? instance;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Path to the DLL file.
- /// The plugin manifest.
- public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
- {
- if (dllFile.Name == "FFXIVClientStructs.Generators.dll")
- {
- // Could this be done another way? Sure. It is an extremely common source
- // of errors in the log through, and should never be loaded as a plugin.
- Log.Error($"Not a plugin: {dllFile.FullName}");
- throw new InvalidPluginException(dllFile);
- }
-
- this.DllFile = dllFile;
- this.State = PluginState.Unloaded;
-
- this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, this.SetupLoaderConfig);
-
- try
- {
- this.pluginAssembly = this.loader.LoadDefaultAssembly();
- }
- catch (Exception ex)
- {
- this.pluginAssembly = null;
- this.pluginType = null;
- this.loader.Dispose();
-
- Log.Error(ex, $"Not a plugin: {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.IsAssignableTo(typeof(IDalamudPlugin)));
- }
-
- if (this.pluginType == default)
- {
- this.pluginAssembly = null;
- this.pluginType = null;
- this.loader.Dispose();
-
- Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}");
- throw new InvalidPluginException(this.DllFile);
- }
-
- var assemblyVersion = this.pluginAssembly.GetName().Version;
-
- // Although it is conditionally used here, we need to set the initial value regardless.
- this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
-
- // If the parameter manifest was null
- if (manifest == null)
- {
- this.Manifest = new LocalPluginManifest()
- {
- Author = "developer",
- Name = Path.GetFileNameWithoutExtension(this.DllFile.Name),
- InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name),
- AssemblyVersion = assemblyVersion,
- Description = string.Empty,
- ApplicableVersion = GameVersion.Any,
- DalamudApiLevel = PluginManager.DalamudApiLevel,
- IsHide = false,
- };
-
- // Save the manifest to disk so there won't be any problems later.
- // We'll update the name property after it can be retrieved from the instance.
- this.Manifest.Save(this.manifestFile);
- }
- else
- {
- this.Manifest = manifest;
- }
-
- // This converts from the ".disabled" file feature to the manifest instead.
- this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
- if (this.disabledFile.Exists)
- {
- this.Manifest.Disabled = true;
- this.disabledFile.Delete();
- }
-
- // This converts from the ".testing" file feature to the manifest instead.
- this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile);
- if (this.testingFile.Exists)
- {
- this.Manifest.Testing = true;
- this.testingFile.Delete();
- }
-
- var pluginManager = Service.Get();
- this.IsBanned = pluginManager.IsManifestBanned(this.Manifest);
- this.BanReason = pluginManager.GetBanReason(this.Manifest);
-
- this.SaveManifest();
- }
-
- ///
- /// Gets the associated with this plugin.
- ///
- public DalamudPluginInterface? DalamudInterface { get; private set; }
-
- ///
- /// Gets the path to the plugin DLL.
- ///
- public FileInfo DllFile { get; }
-
- ///
- /// Gets the plugin manifest, if one exists.
- ///
- public LocalPluginManifest Manifest { get; private set; }
-
- ///
- /// Gets or sets the current state of the plugin.
- ///
- public PluginState State { get; protected set; }
-
- ///
- /// Gets the AssemblyName plugin, populated during .
- ///
- /// Plugin type.
- public AssemblyName? AssemblyName { get; private set; }
-
- ///
- /// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
- ///
- public string Name => this.instance?.Name ?? this.Manifest.Name;
-
- ///
- /// Gets an optional reason, if the plugin is banned.
- ///
- public string BanReason { get; }
-
- ///
- /// Gets a value indicating whether the plugin is loaded and running.
- ///
- public bool IsLoaded => this.State == PluginState.Loaded;
-
- ///
- /// Gets a value indicating whether the plugin is disabled.
- ///
- public bool IsDisabled => this.Manifest.Disabled;
-
- ///
- /// Gets a value indicating whether this plugin's API level is out of date.
- ///
- public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
-
- ///
- /// Gets a value indicating whether the plugin is for testing use only.
- ///
- public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
-
- ///
- /// Gets a value indicating whether this plugin has been banned.
- ///
- public bool IsBanned { get; }
-
- ///
- /// Gets a value indicating whether this plugin is dev plugin.
- ///
- public bool IsDev => this is LocalDevPlugin;
-
- ///
- public void Dispose()
- {
- this.instance?.Dispose();
- this.instance = null;
-
- this.DalamudInterface?.ExplicitDispose();
- this.DalamudInterface = null;
-
- this.pluginType = null;
- this.pluginAssembly = null;
-
- this.loader?.Dispose();
- }
-
- ///
- /// Load this plugin.
- ///
- /// The reason why this plugin is being loaded.
- /// Load while reloading.
- public void Load(PluginLoadReason reason, bool reloading = false)
- {
- var startInfo = Service.Get();
- var configuration = Service.Get();
- var pluginManager = Service.Get();
-
- // Allowed: Unloaded
- switch (this.State)
- {
- case PluginState.InProgress:
- throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working");
- case PluginState.Loaded:
- throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded");
- case PluginState.LoadError:
- throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first");
- case PluginState.UnloadError:
- throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
- }
-
- if (pluginManager.IsManifestBanned(this.Manifest))
- throw new BannedPluginException($"Unable to load {this.Name}, banned");
-
- if (this.Manifest.ApplicableVersion < startInfo.GameVersion)
- throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
-
- if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.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");
-
- this.State = PluginState.InProgress;
- Log.Information($"Loading {this.DllFile.Name}");
-
- if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll")))
- {
- Log.Error("==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", this.Manifest.Author!, this.Manifest.InternalName);
- Log.Error("YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!");
- Log.Error("You may not be able to load your plugin. \"False\" needs to be set in your csproj.");
- Log.Error("If you are using ILMerge, do not merge anything other than your direct dependencies.");
- Log.Error("Do not merge FFXIVClientStructs.Generators.dll.");
- Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
- }
-
- try
- {
- this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, this.SetupLoaderConfig);
-
- if (reloading || this.IsDev)
- {
- if (this.IsDev)
- {
- // If a dev plugin is set to not autoload on boot, but we want to reload it at the arbitrary load
- // time, we need to essentially "Unload" the plugin, but we can't call plugin.Unload because of the
- // load state checks. Null any references to the assembly and types, then proceed with regular reload
- // operations.
- this.pluginAssembly = null;
- this.pluginType = null;
- }
-
- this.loader.Reload();
-
- if (this.IsDev)
- {
- // Reload the manifest in-case there were changes here too.
- var manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
- if (manifestFile.Exists)
- {
- this.Manifest = LocalPluginManifest.Load(manifestFile);
- }
- }
- }
-
- // 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)));
-
- // Check for any loaded plugins with the same assembly 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
- if (otherPlugin == this || otherPlugin.instance == null)
- continue;
-
- var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name;
- if (otherPluginAssemblyName == assemblyName)
- {
- this.State = PluginState.Unloaded;
- Log.Debug($"Duplicate assembly: {this.Name}");
-
- throw new DuplicatePluginException(assemblyName);
- }
- }
-
- // Update the location for the Location and CodeBase patches
- PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new(this.DllFile);
-
- this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev);
-
- var ioc = Service.Get();
- this.instance = ioc.Create(this.pluginType, this.DalamudInterface) as IDalamudPlugin;
- if (this.instance == null)
- {
- this.State = PluginState.LoadError;
- this.DalamudInterface.ExplicitDispose();
- Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor");
- return;
- }
-
- SignatureHelper.Initialise(this.instance);
-
- // In-case the manifest name was a placeholder. Can occur when no manifest was included.
- if (this.instance.Name != this.Manifest.Name)
- {
- this.Manifest.Name = this.instance.Name;
- this.Manifest.Save(this.manifestFile);
- }
-
- this.State = PluginState.Loaded;
- Log.Information($"Finished loading {this.DllFile.Name}");
- }
- catch (Exception ex)
- {
- this.State = PluginState.LoadError;
- Log.Error(ex, $"Error while loading {this.Name}");
-
- throw;
- }
- }
-
- ///
- /// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay
- /// in the plugin list until it has been actually disposed.
- ///
- /// Unload while reloading.
- public void Unload(bool reloading = false)
- {
- // Allowed: Loaded, LoadError(we are cleaning this up while we're at it)
- switch (this.State)
- {
- case PluginState.InProgress:
- throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working");
- case PluginState.Unloaded:
- throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
- case PluginState.UnloadError:
- throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud");
- }
-
- try
- {
- this.State = PluginState.InProgress;
- Log.Information($"Unloading {this.DllFile.Name}");
-
- this.instance?.Dispose();
- this.instance = null;
-
- this.DalamudInterface?.ExplicitDispose();
- this.DalamudInterface = null;
-
- this.pluginType = null;
- this.pluginAssembly = null;
-
- if (!reloading)
- {
- this.loader?.Dispose();
- this.loader = null;
- }
-
- this.State = PluginState.Unloaded;
- Log.Information($"Finished unloading {this.DllFile.Name}");
- }
- catch (Exception ex)
- {
- this.State = PluginState.UnloadError;
- Log.Error(ex, $"Error while unloading {this.Name}");
-
- throw;
- }
- }
-
- ///
- /// Reload this plugin.
- ///
- public void Reload()
- {
- this.Unload(true);
-
- // 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.Get();
- dtr.HandleRemovedNodes();
-
- this.Load(PluginLoadReason.Reload, true);
- }
-
- ///
- /// Revert a disable. Must be unloaded first, does not load.
- ///
- public void Enable()
- {
- // Allowed: Unloaded, UnloadError
- switch (this.State)
- {
- case PluginState.InProgress:
- case PluginState.Loaded:
- case PluginState.LoadError:
- throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
- }
-
- if (!this.Manifest.Disabled)
- throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
-
- this.Manifest.Disabled = false;
- this.SaveManifest();
- }
-
- ///
- /// Disable this plugin, must be unloaded first.
- ///
- public void Disable()
- {
- // Allowed: Unloaded, UnloadError
- switch (this.State)
- {
- case PluginState.InProgress:
- case PluginState.Loaded:
- case PluginState.LoadError:
- throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");
- }
-
- if (this.Manifest.Disabled)
- throw new InvalidPluginOperationException($"Unable to disable {this.Name}, already disabled");
-
- this.Manifest.Disabled = true;
- this.SaveManifest();
- }
-
- private void SetupLoaderConfig(LoaderConfig config)
- {
- config.IsUnloadable = true;
- config.LoadInMemory = true;
- config.PreferSharedTypes = false;
- config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName());
- config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName());
- }
-
- private void SaveManifest() => this.Manifest.Save(this.manifestFile);
- }
-}
diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs
index f98e122f5..25ec9a11c 100644
--- a/Dalamud/Plugin/Internal/PluginManager.cs
+++ b/Dalamud/Plugin/Internal/PluginManager.cs
@@ -21,441 +21,303 @@ using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using Newtonsoft.Json;
-namespace Dalamud.Plugin.Internal
+namespace Dalamud.Plugin.Internal;
+
+///
+/// Class responsible for loading and unloading plugins.
+///
+internal partial class PluginManager : IDisposable
{
///
- /// Class responsible for loading and unloading plugins.
+ /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded.
///
- internal partial class PluginManager : IDisposable
+ public const int DalamudApiLevel = 6;
+
+ private static readonly ModuleLog Log = new("PLUGINM");
+
+ private readonly DirectoryInfo pluginDirectory;
+ private readonly DirectoryInfo devPluginDirectory;
+ private readonly BannedPlugin[] bannedPlugins;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PluginManager()
{
- ///
- /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded.
- ///
- public const int DalamudApiLevel = 6;
+ var startInfo = Service.Get();
+ var configuration = Service.Get();
- private static readonly ModuleLog Log = new("PLUGINM");
+ this.pluginDirectory = new DirectoryInfo(startInfo.PluginDirectory);
+ this.devPluginDirectory = new DirectoryInfo(startInfo.DefaultPluginDirectory);
- private readonly DirectoryInfo pluginDirectory;
- private readonly DirectoryInfo devPluginDirectory;
- private readonly BannedPlugin[] bannedPlugins;
+ if (!this.pluginDirectory.Exists)
+ this.pluginDirectory.Create();
- ///
- /// Initializes a new instance of the class.
- ///
- public PluginManager()
+ if (!this.devPluginDirectory.Exists)
+ this.devPluginDirectory.Create();
+
+ this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || configuration.PluginSafeMode;
+ if (this.SafeMode)
{
- var startInfo = Service.Get();
- var configuration = Service.Get();
-
- this.pluginDirectory = new DirectoryInfo(startInfo.PluginDirectory);
- this.devPluginDirectory = new DirectoryInfo(startInfo.DefaultPluginDirectory);
-
- if (!this.pluginDirectory.Exists)
- this.pluginDirectory.Create();
-
- if (!this.devPluginDirectory.Exists)
- this.devPluginDirectory.Create();
-
- this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || configuration.PluginSafeMode;
- if (this.SafeMode)
- {
- configuration.PluginSafeMode = false;
- configuration.Save();
- }
-
- this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath) ?? string.Empty, "pluginConfigs"));
-
- var bannedPluginsJson = File.ReadAllText(Path.Combine(startInfo.AssetDirectory, "UIRes", "bannedplugin.json"));
- this.bannedPlugins = JsonConvert.DeserializeObject(bannedPluginsJson) ?? Array.Empty();
-
- this.ApplyPatches();
+ configuration.PluginSafeMode = false;
+ configuration.Save();
}
- ///
- /// An event that fires when the installed plugins have changed.
- ///
- public event Action? OnInstalledPluginsChanged;
+ this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath) ?? string.Empty, "pluginConfigs"));
- ///
- /// An event that fires when the available plugins have changed.
- ///
- public event Action? OnAvailablePluginsChanged;
+ var bannedPluginsJson = File.ReadAllText(Path.Combine(startInfo.AssetDirectory, "UIRes", "bannedplugin.json"));
+ this.bannedPlugins = JsonConvert.DeserializeObject(bannedPluginsJson) ?? Array.Empty();
- ///
- /// Gets a list of all loaded plugins.
- ///
- public ImmutableList InstalledPlugins { get; private set; } = ImmutableList.Create();
+ this.ApplyPatches();
+ }
- ///
- /// Gets a list of all available plugins.
- ///
- public ImmutableList AvailablePlugins { get; private set; } = ImmutableList.Create();
+ ///
+ /// An event that fires when the installed plugins have changed.
+ ///
+ public event Action? OnInstalledPluginsChanged;
- ///
- /// Gets a list of all plugins with an available update.
- ///
- public ImmutableList UpdatablePlugins { get; private set; } = ImmutableList.Create();
+ ///
+ /// An event that fires when the available plugins have changed.
+ ///
+ public event Action? OnAvailablePluginsChanged;
- ///
- /// Gets a list of all plugin repositories. The main repo should always be first.
- ///
- public List Repos { get; private set; } = new();
+ ///
+ /// Gets a list of all loaded plugins.
+ ///
+ public ImmutableList InstalledPlugins { get; private set; } = ImmutableList.Create();
- ///
- /// Gets a value indicating whether plugins are not still loading from boot.
- ///
- public bool PluginsReady { get; private set; }
+ ///
+ /// Gets a list of all available plugins.
+ ///
+ public ImmutableList AvailablePlugins { get; private set; } = ImmutableList.Create();
- ///
- /// Gets a value indicating whether all added repos are not in progress.
- ///
- public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress);
+ ///
+ /// Gets a list of all plugins with an available update.
+ ///
+ public ImmutableList UpdatablePlugins { get; private set; } = ImmutableList.Create();
- ///
- /// Gets a value indicating whether the plugin manager started in safe mode.
- ///
- public bool SafeMode { get; init; }
+ ///
+ /// Gets a list of all plugin repositories. The main repo should always be first.
+ ///
+ public List Repos { get; private set; } = new();
- ///
- /// Gets the object used when initializing plugins.
- ///
- public PluginConfigurations PluginConfigs { get; }
+ ///
+ /// Gets a value indicating whether plugins are not still loading from boot.
+ ///
+ public bool PluginsReady { get; private set; }
- ///
- /// Print to chat any plugin updates and whether they were successful.
- ///
- /// The list of updated plugin metadata.
- /// The header text to send to chat prior to any update info.
- public static void PrintUpdatedPlugins(List? updateMetadata, string header)
+ ///
+ /// Gets a value indicating whether all added repos are not in progress.
+ ///
+ public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress);
+
+ ///
+ /// Gets a value indicating whether the plugin manager started in safe mode.
+ ///
+ public bool SafeMode { get; init; }
+
+ ///
+ /// Gets the object used when initializing plugins.
+ ///
+ public PluginConfigurations PluginConfigs { get; }
+
+ ///
+ /// Print to chat any plugin updates and whether they were successful.
+ ///
+ /// The list of updated plugin metadata.
+ /// The header text to send to chat prior to any update info.
+ public static void PrintUpdatedPlugins(List? updateMetadata, string header)
+ {
+ var chatGui = Service.Get();
+
+ if (updateMetadata is { Count: > 0 })
{
- var chatGui = Service.Get();
+ chatGui.Print(header);
- if (updateMetadata is { Count: > 0 })
+ foreach (var metadata in updateMetadata)
{
- chatGui.Print(header);
-
- foreach (var metadata in updateMetadata)
+ if (metadata.WasUpdated)
{
- if (metadata.WasUpdated)
+ chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version));
+ }
+ else
+ {
+ chatGui.PrintChat(new XivChatEntry
{
- chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version));
- }
- else
- {
- chatGui.PrintChat(new XivChatEntry
- {
- Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version),
- Type = XivChatType.Urgent,
- });
- }
+ Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version),
+ Type = XivChatType.Urgent,
+ });
}
}
}
+ }
- ///
- /// For a given manifest, determine if the testing version should be used over the normal version.
- /// The higher of the two versions is calculated after checking other settings.
- ///
- /// Manifest to check.
- /// A value indicating whether testing should be used.
- public static bool UseTesting(PluginManifest manifest)
- {
- var configuration = Service.Get();
-
- if (!configuration.DoPluginTest)
- return false;
-
- if (manifest.IsTestingExclusive)
- return true;
-
- var av = manifest.AssemblyVersion;
- var tv = manifest.TestingAssemblyVersion;
- var hasTv = tv != null;
-
- if (hasTv)
- {
- return tv > av;
- }
+ ///
+ /// For a given manifest, determine if the testing version should be used over the normal version.
+ /// The higher of the two versions is calculated after checking other settings.
+ ///
+ /// Manifest to check.
+ /// A value indicating whether testing should be used.
+ public static bool UseTesting(PluginManifest manifest)
+ {
+ var configuration = Service.Get();
+ if (!configuration.DoPluginTest)
return false;
+
+ if (manifest.IsTestingExclusive)
+ return true;
+
+ var av = manifest.AssemblyVersion;
+ var tv = manifest.TestingAssemblyVersion;
+ var hasTv = tv != null;
+
+ if (hasTv)
+ {
+ return tv > av;
}
- ///
- /// Gets a value indicating whether the given repo manifest should be visible to the user.
- ///
- /// Repo manifest.
- /// If the manifest is visible.
- public static bool IsManifestVisible(RemotePluginManifest manifest)
+ return false;
+ }
+
+ ///
+ /// Gets a value indicating whether the given repo manifest should be visible to the user.
+ ///
+ /// Repo manifest.
+ /// If the manifest is visible.
+ public static bool IsManifestVisible(RemotePluginManifest manifest)
+ {
+ var configuration = Service.Get();
+
+ // Hidden by user
+ if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName))
+ return false;
+
+ // Hidden by manifest
+ return !manifest.IsHide;
+ }
+
+ ///
+ public void Dispose()
+ {
+ foreach (var plugin in this.InstalledPlugins)
{
- var configuration = Service.Get();
-
- // Hidden by user
- if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName))
- return false;
-
- // Hidden by manifest
- return !manifest.IsHide;
+ try
+ {
+ plugin.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error disposing {plugin.Name}");
+ }
}
- ///
- public void Dispose()
+ this.assemblyLocationMonoHook?.Dispose();
+ this.assemblyCodeBaseMonoHook?.Dispose();
+ }
+
+ ///
+ /// Set the list of repositories to use and downloads their contents.
+ /// Should be called when the Settings window has been updated or at instantiation.
+ ///
+ /// Whether the available plugins changed event should be sent after.
+ /// A representing the asynchronous operation.
+ public async Task SetPluginReposFromConfigAsync(bool notify)
+ {
+ var configuration = Service.Get();
+
+ var repos = new List() { PluginRepository.MainRepo };
+ repos.AddRange(configuration.ThirdRepoList
+ .Where(repo => repo.IsEnabled)
+ .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled)));
+
+ this.Repos = repos;
+ await this.ReloadPluginMastersAsync(notify);
+ }
+
+ ///
+ /// Load all plugins, sorted by priority. Any plugins with no explicit definition file or a negative priority
+ /// are loaded asynchronously.
+ ///
+ ///
+ /// This should only be called during Dalamud startup.
+ ///
+ public void LoadAllPlugins()
+ {
+ if (this.SafeMode)
{
- foreach (var plugin in this.InstalledPlugins)
+ Log.Information("PluginSafeMode was enabled, not loading any plugins.");
+ return;
+ }
+
+ var configuration = Service.Get();
+
+ var pluginDefs = new List();
+ var devPluginDefs = new List();
+
+ if (!this.pluginDirectory.Exists)
+ this.pluginDirectory.Create();
+
+ if (!this.devPluginDirectory.Exists)
+ this.devPluginDirectory.Create();
+
+ // Add installed plugins. These are expected to be in a specific format so we can look for exactly that.
+ foreach (var pluginDir in this.pluginDirectory.GetDirectories())
+ {
+ foreach (var versionDir in pluginDir.GetDirectories())
+ {
+ var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+
+ if (!manifestFile.Exists)
+ continue;
+
+ var manifest = LocalPluginManifest.Load(manifestFile);
+
+ pluginDefs.Add(new PluginDef(dllFile, manifest, false));
+ }
+ }
+
+ // devPlugins are more freeform. Look for any dll and hope to get lucky.
+ var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
+
+ foreach (var setting in configuration.DevPluginLoadLocations)
+ {
+ if (!setting.IsEnabled)
+ continue;
+
+ if (Directory.Exists(setting.Path))
+ {
+ devDllFiles.AddRange(new DirectoryInfo(setting.Path).GetFiles("*.dll", SearchOption.AllDirectories));
+ }
+ else if (File.Exists(setting.Path))
+ {
+ devDllFiles.Add(new FileInfo(setting.Path));
+ }
+ }
+
+ foreach (var dllFile in devDllFiles)
+ {
+ // Manifests are not required for devPlugins. the Plugin type will handle any null manifests.
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+ var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null;
+ devPluginDefs.Add(new PluginDef(dllFile, manifest, true));
+ }
+
+ // Sort for load order - unloaded definitions have default priority of 0
+ pluginDefs.Sort(PluginDef.Sorter);
+ devPluginDefs.Sort(PluginDef.Sorter);
+
+ // Dev plugins should load first.
+ pluginDefs.InsertRange(0, devPluginDefs);
+
+ void LoadPlugins(IEnumerable pluginDefsList)
+ {
+ foreach (var pluginDef in pluginDefsList)
{
try
{
- plugin.Dispose();
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"Error disposing {plugin.Name}");
- }
- }
-
- this.assemblyLocationMonoHook?.Dispose();
- this.assemblyCodeBaseMonoHook?.Dispose();
- }
-
- ///
- /// Set the list of repositories to use and downloads their contents.
- /// Should be called when the Settings window has been updated or at instantiation.
- ///
- /// Whether the available plugins changed event should be sent after.
- /// A representing the asynchronous operation.
- public async Task SetPluginReposFromConfigAsync(bool notify)
- {
- var configuration = Service.Get();
-
- var repos = new List() { PluginRepository.MainRepo };
- repos.AddRange(configuration.ThirdRepoList
- .Where(repo => repo.IsEnabled)
- .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled)));
-
- this.Repos = repos;
- await this.ReloadPluginMastersAsync(notify);
- }
-
- ///
- /// Load all plugins, sorted by priority. Any plugins with no explicit definition file or a negative priority
- /// are loaded asynchronously.
- ///
- ///
- /// This should only be called during Dalamud startup.
- ///
- public void LoadAllPlugins()
- {
- if (this.SafeMode)
- {
- Log.Information("PluginSafeMode was enabled, not loading any plugins.");
- return;
- }
-
- var configuration = Service.Get();
-
- var pluginDefs = new List();
- var devPluginDefs = new List();
-
- if (!this.pluginDirectory.Exists)
- this.pluginDirectory.Create();
-
- if (!this.devPluginDirectory.Exists)
- this.devPluginDirectory.Create();
-
- // Add installed plugins. These are expected to be in a specific format so we can look for exactly that.
- foreach (var pluginDir in this.pluginDirectory.GetDirectories())
- {
- foreach (var versionDir in pluginDir.GetDirectories())
- {
- var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
- var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
-
- if (!manifestFile.Exists)
- continue;
-
- var manifest = LocalPluginManifest.Load(manifestFile);
-
- pluginDefs.Add(new PluginDef(dllFile, manifest, false));
- }
- }
-
- // devPlugins are more freeform. Look for any dll and hope to get lucky.
- var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
-
- foreach (var setting in configuration.DevPluginLoadLocations)
- {
- if (!setting.IsEnabled)
- continue;
-
- if (Directory.Exists(setting.Path))
- {
- devDllFiles.AddRange(new DirectoryInfo(setting.Path).GetFiles("*.dll", SearchOption.AllDirectories));
- }
- else if (File.Exists(setting.Path))
- {
- devDllFiles.Add(new FileInfo(setting.Path));
- }
- }
-
- foreach (var dllFile in devDllFiles)
- {
- // Manifests are not required for devPlugins. the Plugin type will handle any null manifests.
- var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
- var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null;
- devPluginDefs.Add(new PluginDef(dllFile, manifest, true));
- }
-
- // Sort for load order - unloaded definitions have default priority of 0
- pluginDefs.Sort(PluginDef.Sorter);
- devPluginDefs.Sort(PluginDef.Sorter);
-
- // Dev plugins should load first.
- pluginDefs.InsertRange(0, devPluginDefs);
-
- void LoadPlugins(IEnumerable pluginDefsList)
- {
- foreach (var pluginDef in pluginDefsList)
- {
- try
- {
- this.LoadPlugin(pluginDef.DllFile, pluginDef.Manifest, PluginLoadReason.Boot, pluginDef.IsDev, isBoot: true);
- }
- catch (InvalidPluginException)
- {
- // Not a plugin
- }
- catch (Exception ex)
- {
- Log.Error(ex, "During boot plugin load, an unexpected error occurred");
- }
- }
- }
-
- // Load sync plugins
- var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadPriority > 0);
- LoadPlugins(syncPlugins);
-
- var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0);
- Task.Run(() => LoadPlugins(asyncPlugins))
- .ContinueWith(_ =>
- {
- this.PluginsReady = true;
- this.NotifyInstalledPluginsChanged();
- });
- }
-
- ///
- /// Reload all loaded plugins.
- ///
- public void ReloadAllPlugins()
- {
- var aggregate = new List();
-
- foreach (var plugin in this.InstalledPlugins)
- {
- if (plugin.IsLoaded)
- {
- try
- {
- plugin.Reload();
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Error during reload all");
-
- aggregate.Add(ex);
- }
- }
- }
-
- if (aggregate.Any())
- {
- throw new AggregateException(aggregate);
- }
- }
-
- ///
- /// Reload the PluginMaster for each repo, filter, and event that the list has updated.
- ///
- /// Whether to notify that available plugins have changed afterwards.
- /// A representing the asynchronous operation.
- public async Task ReloadPluginMastersAsync(bool notify = true)
- {
- await Task.WhenAll(this.Repos.Select(repo => repo.ReloadPluginMasterAsync()));
-
- this.RefilterPluginMasters(notify);
- }
-
- ///
- /// Apply visibility and eligibility filters to the available plugins, then event that the list has updated.
- ///
- /// Whether to notify that available plugins have changed afterwards.
- public void RefilterPluginMasters(bool notify = true)
- {
- this.AvailablePlugins = this.Repos
- .SelectMany(repo => repo.PluginMaster)
- .Where(this.IsManifestEligible)
- .Where(IsManifestVisible)
- .ToImmutableList();
-
- if (notify)
- {
- this.NotifyAvailablePluginsChanged();
- }
- }
-
- ///
- /// Scan the devPlugins folder for new DLL files that are not already loaded into the manager. They are not loaded,
- /// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works
- /// a little differently.
- ///
- public void ScanDevPlugins()
- {
- if (this.SafeMode)
- {
- Log.Information("PluginSafeMode was enabled, not scanning any dev plugins.");
- return;
- }
-
- var configuration = Service.Get();
-
- if (!this.devPluginDirectory.Exists)
- this.devPluginDirectory.Create();
-
- // devPlugins are more freeform. Look for any dll and hope to get lucky.
- var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
-
- foreach (var setting in configuration.DevPluginLoadLocations)
- {
- if (!setting.IsEnabled)
- continue;
-
- if (Directory.Exists(setting.Path))
- {
- devDllFiles.AddRange(new DirectoryInfo(setting.Path).GetFiles("*.dll", SearchOption.AllDirectories));
- }
- else if (File.Exists(setting.Path))
- {
- devDllFiles.Add(new FileInfo(setting.Path));
- }
- }
-
- var listChanged = false;
-
- foreach (var dllFile in devDllFiles)
- {
- // This file is already known to us
- if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName))
- continue;
-
- // Manifests are not required for devPlugins. the Plugin type will handle any null manifests.
- var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
- var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null;
-
- try
- {
- // Add them to the list and let the user decide, nothing is auto-loaded.
- this.LoadPlugin(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true);
- listChanged = true;
+ this.LoadPlugin(pluginDef.DllFile, pluginDef.Manifest, PluginLoadReason.Boot, pluginDef.IsDev, isBoot: true);
}
catch (InvalidPluginException)
{
@@ -463,675 +325,812 @@ namespace Dalamud.Plugin.Internal
}
catch (Exception ex)
{
- Log.Error(ex, $"During devPlugin scan, an unexpected error occurred");
+ Log.Error(ex, "During boot plugin load, an unexpected error occurred");
}
}
+ }
- if (listChanged)
+ // Load sync plugins
+ var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadPriority > 0);
+ LoadPlugins(syncPlugins);
+
+ var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0);
+ Task.Run(() => LoadPlugins(asyncPlugins))
+ .ContinueWith(_ =>
+ {
+ this.PluginsReady = true;
this.NotifyInstalledPluginsChanged();
- }
+ });
+ }
- ///
- /// Install a plugin from a repository and load it.
- ///
- /// The plugin definition.
- /// If the testing version should be used.
- /// The reason this plugin was loaded.
- /// A representing the asynchronous operation.
- public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason)
+ ///
+ /// Reload all loaded plugins.
+ ///
+ public void ReloadAllPlugins()
+ {
+ var aggregate = new List();
+
+ foreach (var plugin in this.InstalledPlugins)
{
- Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})");
-
- var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
- var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion;
-
- var response = await Util.HttpClient.GetAsync(downloadUrl);
- response.EnsureSuccessStatusCode();
-
- var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty));
-
- try
- {
- if (outputDir.Exists)
- outputDir.Delete(true);
-
- outputDir.Create();
- }
- catch
- {
- // ignored, since the plugin may be loaded already
- }
-
- Log.Debug($"Extracting to {outputDir}");
- // This throws an error, even with overwrite=false
- // ZipFile.ExtractToDirectory(tempZip.FullName, outputDir.FullName, false);
- using (var archive = new ZipArchive(await response.Content.ReadAsStreamAsync()))
- {
- foreach (var zipFile in archive.Entries)
- {
- var outputFile = new FileInfo(Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName)));
-
- if (!outputFile.FullName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase))
- {
- throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability");
- }
-
- if (outputFile.Directory == null)
- {
- throw new IOException("Output directory invalid.");
- }
-
- if (zipFile.Name.IsNullOrEmpty())
- {
- // Assuming Empty for Directory
- Log.Verbose($"ZipFile name is null or empty, treating as a directory: {outputFile.Directory.FullName}");
- Directory.CreateDirectory(outputFile.Directory.FullName);
- continue;
- }
-
- // Ensure directory is created
- Directory.CreateDirectory(outputFile.Directory.FullName);
-
- try
- {
- zipFile.ExtractToFile(outputFile.FullName, true);
- }
- catch (Exception ex)
- {
- if (outputFile.Extension.EndsWith("dll"))
- {
- throw new IOException($"Could not overwrite {zipFile.Name}: {ex.Message}");
- }
-
- Log.Error($"Could not overwrite {zipFile.Name}: {ex.Message}");
- }
- }
- }
-
- var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest);
- var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
-
- // We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use.
- File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented));
-
- // Reload as a local manifest, add some attributes, and save again.
- var manifest = LocalPluginManifest.Load(manifestFile);
-
- if (useTesting)
- {
- manifest.Testing = true;
- }
-
- if (repoManifest.SourceRepo.IsThirdParty)
- {
- // Only document the url if it came from a third party repo.
- manifest.InstalledFromUrl = repoManifest.SourceRepo.PluginMasterUrl;
- }
-
- manifest.Save(manifestFile);
-
- Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})");
-
- var plugin = this.LoadPlugin(dllFile, manifest, reason);
-
- this.NotifyInstalledPluginsChanged();
- return plugin;
- }
-
- ///
- /// Load a plugin.
- ///
- /// The associated with the main assembly of this plugin.
- /// The already loaded definition, if available.
- /// The reason this plugin was loaded.
- /// If this plugin should support development features.
- /// If this plugin is being loaded at boot.
- /// Don't load the plugin, just don't do it.
- /// The loaded plugin.
- public LocalPlugin LoadPlugin(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 (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)
+ if (plugin.IsLoaded)
{
try
{
- if (plugin.IsDisabled)
- plugin.Enable();
-
- plugin.Load(reason);
+ plugin.Reload();
}
- catch (InvalidPluginException)
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during reload all");
+
+ aggregate.Add(ex);
+ }
+ }
+ }
+
+ if (aggregate.Any())
+ {
+ throw new AggregateException(aggregate);
+ }
+ }
+
+ ///
+ /// Reload the PluginMaster for each repo, filter, and event that the list has updated.
+ ///
+ /// Whether to notify that available plugins have changed afterwards.
+ /// A representing the asynchronous operation.
+ public async Task ReloadPluginMastersAsync(bool notify = true)
+ {
+ await Task.WhenAll(this.Repos.Select(repo => repo.ReloadPluginMasterAsync()));
+
+ this.RefilterPluginMasters(notify);
+ }
+
+ ///
+ /// Apply visibility and eligibility filters to the available plugins, then event that the list has updated.
+ ///
+ /// Whether to notify that available plugins have changed afterwards.
+ public void RefilterPluginMasters(bool notify = true)
+ {
+ this.AvailablePlugins = this.Repos
+ .SelectMany(repo => repo.PluginMaster)
+ .Where(this.IsManifestEligible)
+ .Where(IsManifestVisible)
+ .ToImmutableList();
+
+ if (notify)
+ {
+ this.NotifyAvailablePluginsChanged();
+ }
+ }
+
+ ///
+ /// Scan the devPlugins folder for new DLL files that are not already loaded into the manager. They are not loaded,
+ /// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works
+ /// a little differently.
+ ///
+ public void ScanDevPlugins()
+ {
+ if (this.SafeMode)
+ {
+ Log.Information("PluginSafeMode was enabled, not scanning any dev plugins.");
+ return;
+ }
+
+ var configuration = Service.Get();
+
+ if (!this.devPluginDirectory.Exists)
+ this.devPluginDirectory.Create();
+
+ // devPlugins are more freeform. Look for any dll and hope to get lucky.
+ var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
+
+ foreach (var setting in configuration.DevPluginLoadLocations)
+ {
+ if (!setting.IsEnabled)
+ continue;
+
+ if (Directory.Exists(setting.Path))
+ {
+ devDllFiles.AddRange(new DirectoryInfo(setting.Path).GetFiles("*.dll", SearchOption.AllDirectories));
+ }
+ else if (File.Exists(setting.Path))
+ {
+ devDllFiles.Add(new FileInfo(setting.Path));
+ }
+ }
+
+ var listChanged = false;
+
+ foreach (var dllFile in devDllFiles)
+ {
+ // This file is already known to us
+ if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName))
+ continue;
+
+ // Manifests are not required for devPlugins. the Plugin type will handle any null manifests.
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+ var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null;
+
+ try
+ {
+ // Add them to the list and let the user decide, nothing is auto-loaded.
+ this.LoadPlugin(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true);
+ listChanged = true;
+ }
+ catch (InvalidPluginException)
+ {
+ // Not a plugin
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"During devPlugin scan, an unexpected error occurred");
+ }
+ }
+
+ if (listChanged)
+ this.NotifyInstalledPluginsChanged();
+ }
+
+ ///
+ /// Install a plugin from a repository and load it.
+ ///
+ /// The plugin definition.
+ /// If the testing version should be used.
+ /// The reason this plugin was loaded.
+ /// A representing the asynchronous operation.
+ public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason)
+ {
+ Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})");
+
+ var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
+ var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion;
+
+ var response = await Util.HttpClient.GetAsync(downloadUrl);
+ response.EnsureSuccessStatusCode();
+
+ var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty));
+
+ try
+ {
+ if (outputDir.Exists)
+ outputDir.Delete(true);
+
+ outputDir.Create();
+ }
+ catch
+ {
+ // ignored, since the plugin may be loaded already
+ }
+
+ Log.Debug($"Extracting to {outputDir}");
+ // This throws an error, even with overwrite=false
+ // ZipFile.ExtractToDirectory(tempZip.FullName, outputDir.FullName, false);
+ using (var archive = new ZipArchive(await response.Content.ReadAsStreamAsync()))
+ {
+ foreach (var zipFile in archive.Entries)
+ {
+ var outputFile = new FileInfo(Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName)));
+
+ if (!outputFile.FullName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability");
+ }
+
+ if (outputFile.Directory == null)
+ {
+ throw new IOException("Output directory invalid.");
+ }
+
+ if (zipFile.Name.IsNullOrEmpty())
+ {
+ // Assuming Empty for Directory
+ Log.Verbose($"ZipFile name is null or empty, treating as a directory: {outputFile.Directory.FullName}");
+ Directory.CreateDirectory(outputFile.Directory.FullName);
+ continue;
+ }
+
+ // Ensure directory is created
+ Directory.CreateDirectory(outputFile.Directory.FullName);
+
+ try
+ {
+ zipFile.ExtractToFile(outputFile.FullName, true);
+ }
+ catch (Exception ex)
+ {
+ if (outputFile.Extension.EndsWith("dll"))
+ {
+ throw new IOException($"Could not overwrite {zipFile.Name}: {ex.Message}");
+ }
+
+ Log.Error($"Could not overwrite {zipFile.Name}: {ex.Message}");
+ }
+ }
+ }
+
+ var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest);
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+
+ // We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use.
+ File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented));
+
+ // Reload as a local manifest, add some attributes, and save again.
+ var manifest = LocalPluginManifest.Load(manifestFile);
+
+ if (useTesting)
+ {
+ manifest.Testing = true;
+ }
+
+ if (repoManifest.SourceRepo.IsThirdParty)
+ {
+ // Only document the url if it came from a third party repo.
+ manifest.InstalledFromUrl = repoManifest.SourceRepo.PluginMasterUrl;
+ }
+
+ manifest.Save(manifestFile);
+
+ Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})");
+
+ var plugin = this.LoadPlugin(dllFile, manifest, reason);
+
+ this.NotifyInstalledPluginsChanged();
+ return plugin;
+ }
+
+ ///
+ /// Load a plugin.
+ ///
+ /// The associated with the main assembly of this plugin.
+ /// The already loaded definition, if available.
+ /// The reason this plugin was loaded.
+ /// If this plugin should support development features.
+ /// If this plugin is being loaded at boot.
+ /// Don't load the plugin, just don't do it.
+ /// The loaded plugin.
+ public LocalPlugin LoadPlugin(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 (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.Enable();
+
+ plugin.Load(reason);
+ }
+ catch (InvalidPluginException)
+ {
+ PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty);
+ 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}");
+ 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
{
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty);
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}");
- 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
- {
- PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty);
- throw;
- }
- }
- }
-
- this.InstalledPlugins = this.InstalledPlugins.Add(plugin);
- return plugin;
- }
-
- ///
- /// Remove a plugin.
- ///
- /// Plugin to remove.
- public void RemovePlugin(LocalPlugin plugin)
- {
- if (plugin.State != PluginState.Unloaded)
- throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded");
-
- this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
- PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty);
-
- this.NotifyInstalledPluginsChanged();
- this.NotifyAvailablePluginsChanged();
- }
-
- ///
- /// Cleanup disabled plugins. Does not target devPlugins.
- ///
- public void CleanupPlugins()
- {
- var configuration = Service.Get();
- var startInfo = Service.Get();
-
- foreach (var pluginDir in this.pluginDirectory.GetDirectories())
- {
- try
- {
- var versionDirs = pluginDir.GetDirectories();
-
- versionDirs = versionDirs
- .OrderByDescending(dir =>
- {
- var isVersion = Version.TryParse(dir.Name, out var version);
-
- if (!isVersion)
- {
- Log.Debug($"Not a version, cleaning up {dir.FullName}");
- dir.Delete(true);
- }
-
- return version;
- })
- .ToArray();
-
- if (versionDirs.Length == 0)
- {
- Log.Information($"No versions: cleaning up {pluginDir.FullName}");
- pluginDir.Delete(true);
- }
- else
- {
- foreach (var versionDir in versionDirs)
- {
- try
- {
- var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
- if (!dllFile.Exists)
- {
- Log.Information($"Missing dll: cleaning up {versionDir.FullName}");
- versionDir.Delete(true);
- continue;
- }
-
- var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
- if (!manifestFile.Exists)
- {
- Log.Information($"Missing manifest: cleaning up {versionDir.FullName}");
- versionDir.Delete(true);
- continue;
- }
-
- var manifest = LocalPluginManifest.Load(manifestFile);
- if (manifest.Disabled)
- {
- Log.Information($"Disabled: cleaning up {versionDir.FullName}");
- versionDir.Delete(true);
- continue;
- }
-
- if (manifest.DalamudApiLevel < DalamudApiLevel - 1 && !configuration.LoadAllApiLevels)
- {
- Log.Information($"Lower API: cleaning up {versionDir.FullName}");
- versionDir.Delete(true);
- continue;
- }
-
- if (manifest.ApplicableVersion < startInfo.GameVersion)
- {
- Log.Information($"Inapplicable version: cleaning up {versionDir.FullName}");
- versionDir.Delete(true);
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"Could not clean up {versionDir.FullName}");
- }
- }
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"Could not clean up {pluginDir.FullName}");
- }
}
}
- ///
- /// Update all non-dev plugins.
- ///
- /// Perform a dry run, don't install anything.
- /// Success or failure and a list of updated plugin metadata.
- public async Task> UpdatePluginsAsync(bool dryRun = false)
+ this.InstalledPlugins = this.InstalledPlugins.Add(plugin);
+ return plugin;
+ }
+
+ ///
+ /// Remove a plugin.
+ ///
+ /// Plugin to remove.
+ public void RemovePlugin(LocalPlugin plugin)
+ {
+ if (plugin.State != PluginState.Unloaded)
+ throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded");
+
+ this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
+ PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty);
+
+ this.NotifyInstalledPluginsChanged();
+ this.NotifyAvailablePluginsChanged();
+ }
+
+ ///
+ /// Cleanup disabled plugins. Does not target devPlugins.
+ ///
+ public void CleanupPlugins()
+ {
+ var configuration = Service.Get();
+ var startInfo = Service.Get();
+
+ foreach (var pluginDir in this.pluginDirectory.GetDirectories())
{
- Log.Information("Starting plugin update");
-
- var updatedList = new List();
-
- // Prevent collection was modified errors
- foreach (var plugin in this.UpdatablePlugins)
+ try
{
- // Can't update that!
- if (plugin.InstalledPlugin.IsDev)
- continue;
+ var versionDirs = pluginDir.GetDirectories();
- var result = await this.UpdateSinglePluginAsync(plugin, false, dryRun);
- if (result != null)
- updatedList.Add(result);
- }
+ versionDirs = versionDirs
+ .OrderByDescending(dir =>
+ {
+ var isVersion = Version.TryParse(dir.Name, out var version);
- this.NotifyInstalledPluginsChanged();
+ if (!isVersion)
+ {
+ Log.Debug($"Not a version, cleaning up {dir.FullName}");
+ dir.Delete(true);
+ }
- Log.Information("Plugin update OK.");
+ return version;
+ })
+ .ToArray();
- return updatedList;
- }
-
- ///
- /// Update a single plugin, provided a valid .
- ///
- /// The available plugin update.
- /// Whether to notify that installed plugins have changed afterwards.
- /// Whether or not to actually perform the update, or just indicate success.
- /// The status of the update.
- public async Task UpdateSinglePluginAsync(AvailablePluginUpdate metadata, bool notify, bool dryRun)
- {
- var plugin = metadata.InstalledPlugin;
-
- var updateStatus = new PluginUpdateStatus
- {
- InternalName = plugin.Manifest.InternalName,
- Name = plugin.Manifest.Name,
- Version = (metadata.UseTesting
- ? metadata.UpdateManifest.TestingAssemblyVersion
- : metadata.UpdateManifest.AssemblyVersion)!,
- WasUpdated = true,
- };
-
- if (!dryRun)
- {
- // Unload if loaded
- if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError)
+ if (versionDirs.Length == 0)
{
- try
- {
- plugin.Unload();
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Error during unload (update)");
- updateStatus.WasUpdated = false;
- return updateStatus;
- }
- }
-
- if (plugin.IsDev)
- {
- try
- {
- plugin.DllFile.Delete();
- this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Error during delete (update)");
- updateStatus.WasUpdated = false;
- return updateStatus;
- }
+ Log.Information($"No versions: cleaning up {pluginDir.FullName}");
+ pluginDir.Delete(true);
}
else
{
- try
+ foreach (var versionDir in versionDirs)
{
- plugin.Disable();
- this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Error during disable (update)");
- updateStatus.WasUpdated = false;
- return updateStatus;
+ try
+ {
+ var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
+ if (!dllFile.Exists)
+ {
+ Log.Information($"Missing dll: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+ if (!manifestFile.Exists)
+ {
+ Log.Information($"Missing manifest: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ var manifest = LocalPluginManifest.Load(manifestFile);
+ if (manifest.Disabled)
+ {
+ Log.Information($"Disabled: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ if (manifest.DalamudApiLevel < DalamudApiLevel - 1 && !configuration.LoadAllApiLevels)
+ {
+ Log.Information($"Lower API: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ if (manifest.ApplicableVersion < startInfo.GameVersion)
+ {
+ Log.Information($"Inapplicable version: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Could not clean up {versionDir.FullName}");
+ }
}
}
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Could not clean up {pluginDir.FullName}");
+ }
+ }
+ }
- // 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.Get();
- dtr.HandleRemovedNodes();
+ ///
+ /// Update all non-dev plugins.
+ ///
+ /// Perform a dry run, don't install anything.
+ /// Success or failure and a list of updated plugin metadata.
+ public async Task> UpdatePluginsAsync(bool dryRun = false)
+ {
+ Log.Information("Starting plugin update");
+ var updatedList = new List();
+
+ // Prevent collection was modified errors
+ foreach (var plugin in this.UpdatablePlugins)
+ {
+ // Can't update that!
+ if (plugin.InstalledPlugin.IsDev)
+ continue;
+
+ var result = await this.UpdateSinglePluginAsync(plugin, false, dryRun);
+ if (result != null)
+ updatedList.Add(result);
+ }
+
+ this.NotifyInstalledPluginsChanged();
+
+ Log.Information("Plugin update OK.");
+
+ return updatedList;
+ }
+
+ ///
+ /// Update a single plugin, provided a valid .
+ ///
+ /// The available plugin update.
+ /// Whether to notify that installed plugins have changed afterwards.
+ /// Whether or not to actually perform the update, or just indicate success.
+ /// The status of the update.
+ public async Task UpdateSinglePluginAsync(AvailablePluginUpdate metadata, bool notify, bool dryRun)
+ {
+ var plugin = metadata.InstalledPlugin;
+
+ var updateStatus = new PluginUpdateStatus
+ {
+ InternalName = plugin.Manifest.InternalName,
+ Name = plugin.Manifest.Name,
+ Version = (metadata.UseTesting
+ ? metadata.UpdateManifest.TestingAssemblyVersion
+ : metadata.UpdateManifest.AssemblyVersion)!,
+ WasUpdated = true,
+ };
+
+ if (!dryRun)
+ {
+ // Unload if loaded
+ if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError)
+ {
try
{
- await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update);
+ plugin.Unload();
}
catch (Exception ex)
{
- Log.Error(ex, "Error during install (update)");
+ Log.Error(ex, "Error during unload (update)");
updateStatus.WasUpdated = false;
return updateStatus;
}
}
- if (notify && updateStatus.WasUpdated)
- this.NotifyInstalledPluginsChanged();
-
- return updateStatus;
- }
-
- ///
- /// Unload the plugin, delete its configuration, and reload it.
- ///
- /// The plugin.
- /// Throws if the plugin is still loading/unloading.
- public void DeleteConfiguration(LocalPlugin plugin)
- {
- if (plugin.State == PluginState.InProgress)
- throw new Exception("Cannot delete configuration for a loading/unloading plugin");
-
- if (plugin.IsLoaded)
- plugin.Unload();
-
- // Let's wait so any handles on files in plugin configurations can be closed
- Thread.Sleep(500);
-
- this.PluginConfigs.Delete(plugin.Name);
-
- Thread.Sleep(500);
-
- // Let's indicate "installer" here since this is supposed to be a fresh install
- plugin.Load(PluginLoadReason.Installer);
- }
-
- ///
- /// Gets a value indicating whether the given manifest is eligible for ANYTHING. These are hard
- /// checks that should not allow installation or loading.
- ///
- /// Plugin manifest.
- /// If the manifest is eligible.
- public bool IsManifestEligible(PluginManifest manifest)
- {
- var configuration = Service.Get();
- var startInfo = Service.Get();
-
- // Testing exclusive
- if (manifest.IsTestingExclusive && !configuration.DoPluginTest)
- return false;
-
- // Applicable version
- if (manifest.ApplicableVersion < startInfo.GameVersion)
- return false;
-
- // API level
- if (manifest.DalamudApiLevel < DalamudApiLevel && !configuration.LoadAllApiLevels)
- return false;
-
- // Banned
- return !this.IsManifestBanned(manifest);
- }
-
- ///
- /// Determine if a plugin has been banned by inspecting the manifest.
- ///
- /// Manifest to inspect.
- /// A value indicating whether the plugin/manifest has been banned.
- public bool IsManifestBanned(PluginManifest manifest)
- {
- var configuration = Service.Get();
- return !configuration.LoadBannedPlugins && this.bannedPlugins.Any(ban => (ban.Name == manifest.InternalName || ban.Name == Hash.GetStringSha256Hash(manifest.InternalName))
- && ban.AssemblyVersion >= manifest.AssemblyVersion);
- }
-
- ///
- /// Get the reason of a banned plugin by inspecting the manifest.
- ///
- /// Manifest to inspect.
- /// The reason of the ban, if any.
- public string GetBanReason(PluginManifest manifest)
- {
- return this.bannedPlugins.LastOrDefault(ban => ban.Name == manifest.InternalName).Reason;
- }
-
- private void DetectAvailablePluginUpdates()
- {
- var updatablePlugins = new List();
-
- foreach (var plugin in this.InstalledPlugins)
+ if (plugin.IsDev)
{
- var installedVersion = plugin.IsTesting
- ? plugin.Manifest.TestingAssemblyVersion
- : plugin.Manifest.AssemblyVersion;
-
- var updates = this.AvailablePlugins
- .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName)
- .Select(remoteManifest =>
- {
- var useTesting = UseTesting(remoteManifest);
- var candidateVersion = useTesting
- ? remoteManifest.TestingAssemblyVersion
- : remoteManifest.AssemblyVersion;
- var isUpdate = candidateVersion > installedVersion;
-
- return (isUpdate, useTesting, candidateVersion, remoteManifest);
- })
- .Where(tpl => tpl.isUpdate)
- .ToList();
-
- if (updates.Count > 0)
+ try
{
- var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2);
- updatablePlugins.Add(new AvailablePluginUpdate(plugin, update.remoteManifest, update.useTesting));
+ plugin.DllFile.Delete();
+ this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during delete (update)");
+ updateStatus.WasUpdated = false;
+ return updateStatus;
+ }
+ }
+ else
+ {
+ try
+ {
+ plugin.Disable();
+ this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during disable (update)");
+ updateStatus.WasUpdated = false;
+ return updateStatus;
}
}
- this.UpdatablePlugins = updatablePlugins.ToImmutableList();
- }
-
- private void NotifyAvailablePluginsChanged()
- {
- this.DetectAvailablePluginUpdates();
+ // 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.Get();
+ dtr.HandleRemovedNodes();
try
{
- this.OnAvailablePluginsChanged?.Invoke();
+ await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update);
}
catch (Exception ex)
{
- Log.Error(ex, $"Error notifying {nameof(this.OnAvailablePluginsChanged)}");
+ Log.Error(ex, "Error during install (update)");
+ updateStatus.WasUpdated = false;
+ return updateStatus;
}
}
- private void NotifyInstalledPluginsChanged()
- {
- this.DetectAvailablePluginUpdates();
+ if (notify && updateStatus.WasUpdated)
+ this.NotifyInstalledPluginsChanged();
- try
- {
- this.OnInstalledPluginsChanged?.Invoke();
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"Error notifying {nameof(this.OnInstalledPluginsChanged)}");
- }
- }
-
- private static class Locs
- {
- public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version);
-
- public static string DalamudPluginUpdateFailed(string name, Version version) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed.").Format(name, version);
- }
+ return updateStatus;
}
///
- /// Class responsible for loading and unloading plugins.
- /// This contains the assembly patching functionality to resolve assembly locations.
+ /// Unload the plugin, delete its configuration, and reload it.
///
- internal partial class PluginManager
+ /// The plugin.
+ /// Throws if the plugin is still loading/unloading.
+ public void DeleteConfiguration(LocalPlugin plugin)
{
- ///
- /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading
- /// plugins via byte[].
- ///
- internal static readonly Dictionary PluginLocations = new();
+ if (plugin.State == PluginState.InProgress)
+ throw new Exception("Cannot delete configuration for a loading/unloading plugin");
- private MonoMod.RuntimeDetour.Hook? assemblyLocationMonoHook;
- private MonoMod.RuntimeDetour.Hook? assemblyCodeBaseMonoHook;
+ if (plugin.IsLoaded)
+ plugin.Unload();
- ///
- /// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location.
- /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
- /// It should never be called manually.
- ///
- /// A delegate that acts as the original method.
- /// The equivalent of `this`.
- /// The plugin location, or the result from the original method.
- private static string AssemblyLocationPatch(Func orig, Assembly self)
+ // Let's wait so any handles on files in plugin configurations can be closed
+ Thread.Sleep(500);
+
+ this.PluginConfigs.Delete(plugin.Name);
+
+ Thread.Sleep(500);
+
+ // Let's indicate "installer" here since this is supposed to be a fresh install
+ plugin.Load(PluginLoadReason.Installer);
+ }
+
+ ///
+ /// Gets a value indicating whether the given manifest is eligible for ANYTHING. These are hard
+ /// checks that should not allow installation or loading.
+ ///
+ /// Plugin manifest.
+ /// If the manifest is eligible.
+ public bool IsManifestEligible(PluginManifest manifest)
+ {
+ var configuration = Service.Get();
+ var startInfo = Service.Get();
+
+ // Testing exclusive
+ if (manifest.IsTestingExclusive && !configuration.DoPluginTest)
+ return false;
+
+ // Applicable version
+ if (manifest.ApplicableVersion < startInfo.GameVersion)
+ return false;
+
+ // API level
+ if (manifest.DalamudApiLevel < DalamudApiLevel && !configuration.LoadAllApiLevels)
+ return false;
+
+ // Banned
+ return !this.IsManifestBanned(manifest);
+ }
+
+ ///
+ /// Determine if a plugin has been banned by inspecting the manifest.
+ ///
+ /// Manifest to inspect.
+ /// A value indicating whether the plugin/manifest has been banned.
+ public bool IsManifestBanned(PluginManifest manifest)
+ {
+ var configuration = Service.Get();
+ return !configuration.LoadBannedPlugins && this.bannedPlugins.Any(ban => (ban.Name == manifest.InternalName || ban.Name == Hash.GetStringSha256Hash(manifest.InternalName))
+ && ban.AssemblyVersion >= manifest.AssemblyVersion);
+ }
+
+ ///
+ /// Get the reason of a banned plugin by inspecting the manifest.
+ ///
+ /// Manifest to inspect.
+ /// The reason of the ban, if any.
+ public string GetBanReason(PluginManifest manifest)
+ {
+ return this.bannedPlugins.LastOrDefault(ban => ban.Name == manifest.InternalName).Reason;
+ }
+
+ private void DetectAvailablePluginUpdates()
+ {
+ var updatablePlugins = new List();
+
+ foreach (var plugin in this.InstalledPlugins)
{
- var result = orig(self);
+ var installedVersion = plugin.IsTesting
+ ? plugin.Manifest.TestingAssemblyVersion
+ : plugin.Manifest.AssemblyVersion;
- if (string.IsNullOrEmpty(result))
+ var updates = this.AvailablePlugins
+ .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName)
+ .Select(remoteManifest =>
+ {
+ var useTesting = UseTesting(remoteManifest);
+ var candidateVersion = useTesting
+ ? remoteManifest.TestingAssemblyVersion
+ : remoteManifest.AssemblyVersion;
+ var isUpdate = candidateVersion > installedVersion;
+
+ return (isUpdate, useTesting, candidateVersion, remoteManifest);
+ })
+ .Where(tpl => tpl.isUpdate)
+ .ToList();
+
+ if (updates.Count > 0)
{
- foreach (var assemblyName in GetStackFrameAssemblyNames())
- {
- if (PluginLocations.TryGetValue(assemblyName, out var data))
- {
- result = data.Location;
- break;
- }
- }
- }
-
- result ??= string.Empty;
-
- Log.Verbose($"Assembly.Location // {self.FullName} // {result}");
- return result;
- }
-
- ///
- /// Patch method for internal class RuntimeAssembly.CodeBase, also known as Assembly.CodeBase.
- /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
- /// It should never be called manually.
- ///
- /// A delegate that acts as the original method.
- /// The equivalent of `this`.
- /// The plugin code base, or the result from the original method.
- private static string AssemblyCodeBasePatch(Func orig, Assembly self)
- {
- var result = orig(self);
-
- if (string.IsNullOrEmpty(result))
- {
- foreach (var assemblyName in GetStackFrameAssemblyNames())
- {
- if (PluginLocations.TryGetValue(assemblyName, out var data))
- {
- result = data.CodeBase;
- break;
- }
- }
- }
-
- result ??= string.Empty;
-
- Log.Verbose($"Assembly.CodeBase // {self.FullName} // {result}");
- return result;
- }
-
- private static IEnumerable GetStackFrameAssemblyNames()
- {
- var stackTrace = new StackTrace();
- var stackFrames = stackTrace.GetFrames();
-
- foreach (var stackFrame in stackFrames)
- {
- var methodBase = stackFrame.GetMethod();
- if (methodBase == null)
- continue;
-
- yield return methodBase.Module.Assembly.FullName!;
+ var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2);
+ updatablePlugins.Add(new AvailablePluginUpdate(plugin, update.remoteManifest, update.useTesting));
}
}
- private void ApplyPatches()
+ this.UpdatablePlugins = updatablePlugins.ToImmutableList();
+ }
+
+ private void NotifyAvailablePluginsChanged()
+ {
+ this.DetectAvailablePluginUpdates();
+
+ try
{
- var targetType = typeof(PluginManager).Assembly.GetType();
-
- var locationTarget = targetType.GetProperty(nameof(Assembly.Location))!.GetGetMethod();
- var locationPatch = typeof(PluginManager).GetMethod(nameof(AssemblyLocationPatch), BindingFlags.NonPublic | BindingFlags.Static);
- this.assemblyLocationMonoHook = new MonoMod.RuntimeDetour.Hook(locationTarget, locationPatch);
-
- #pragma warning disable CS0618
- #pragma warning disable SYSLIB0012
- var codebaseTarget = targetType.GetProperty(nameof(Assembly.CodeBase))?.GetGetMethod();
- #pragma warning restore SYSLIB0012
- #pragma warning restore CS0618
- var codebasePatch = typeof(PluginManager).GetMethod(nameof(AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static);
- this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch);
+ this.OnAvailablePluginsChanged?.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error notifying {nameof(this.OnAvailablePluginsChanged)}");
}
}
+
+ private void NotifyInstalledPluginsChanged()
+ {
+ this.DetectAvailablePluginUpdates();
+
+ try
+ {
+ this.OnInstalledPluginsChanged?.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error notifying {nameof(this.OnInstalledPluginsChanged)}");
+ }
+ }
+
+ private static class Locs
+ {
+ public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version);
+
+ public static string DalamudPluginUpdateFailed(string name, Version version) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed.").Format(name, version);
+ }
+}
+
+///
+/// Class responsible for loading and unloading plugins.
+/// This contains the assembly patching functionality to resolve assembly locations.
+///
+internal partial class PluginManager
+{
+ ///
+ /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading
+ /// plugins via byte[].
+ ///
+ internal static readonly Dictionary PluginLocations = new();
+
+ private MonoMod.RuntimeDetour.Hook? assemblyLocationMonoHook;
+ private MonoMod.RuntimeDetour.Hook? assemblyCodeBaseMonoHook;
+
+ ///
+ /// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location.
+ /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
+ /// It should never be called manually.
+ ///
+ /// A delegate that acts as the original method.
+ /// The equivalent of `this`.
+ /// The plugin location, or the result from the original method.
+ private static string AssemblyLocationPatch(Func orig, Assembly self)
+ {
+ var result = orig(self);
+
+ if (string.IsNullOrEmpty(result))
+ {
+ foreach (var assemblyName in GetStackFrameAssemblyNames())
+ {
+ if (PluginLocations.TryGetValue(assemblyName, out var data))
+ {
+ result = data.Location;
+ break;
+ }
+ }
+ }
+
+ result ??= string.Empty;
+
+ Log.Verbose($"Assembly.Location // {self.FullName} // {result}");
+ return result;
+ }
+
+ ///
+ /// Patch method for internal class RuntimeAssembly.CodeBase, also known as Assembly.CodeBase.
+ /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
+ /// It should never be called manually.
+ ///
+ /// A delegate that acts as the original method.
+ /// The equivalent of `this`.
+ /// The plugin code base, or the result from the original method.
+ private static string AssemblyCodeBasePatch(Func orig, Assembly self)
+ {
+ var result = orig(self);
+
+ if (string.IsNullOrEmpty(result))
+ {
+ foreach (var assemblyName in GetStackFrameAssemblyNames())
+ {
+ if (PluginLocations.TryGetValue(assemblyName, out var data))
+ {
+ result = data.CodeBase;
+ break;
+ }
+ }
+ }
+
+ result ??= string.Empty;
+
+ Log.Verbose($"Assembly.CodeBase // {self.FullName} // {result}");
+ return result;
+ }
+
+ private static IEnumerable GetStackFrameAssemblyNames()
+ {
+ var stackTrace = new StackTrace();
+ var stackFrames = stackTrace.GetFrames();
+
+ foreach (var stackFrame in stackFrames)
+ {
+ var methodBase = stackFrame.GetMethod();
+ if (methodBase == null)
+ continue;
+
+ yield return methodBase.Module.Assembly.FullName!;
+ }
+ }
+
+ private void ApplyPatches()
+ {
+ var targetType = typeof(PluginManager).Assembly.GetType();
+
+ var locationTarget = targetType.GetProperty(nameof(Assembly.Location))!.GetGetMethod();
+ var locationPatch = typeof(PluginManager).GetMethod(nameof(AssemblyLocationPatch), BindingFlags.NonPublic | BindingFlags.Static);
+ this.assemblyLocationMonoHook = new MonoMod.RuntimeDetour.Hook(locationTarget, locationPatch);
+
+#pragma warning disable CS0618
+#pragma warning disable SYSLIB0012
+ var codebaseTarget = targetType.GetProperty(nameof(Assembly.CodeBase))?.GetGetMethod();
+#pragma warning restore SYSLIB0012
+#pragma warning restore CS0618
+ var codebasePatch = typeof(PluginManager).GetMethod(nameof(AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static);
+ this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch);
+ }
}
diff --git a/Dalamud/Plugin/Internal/PluginRepository.cs b/Dalamud/Plugin/Internal/PluginRepository.cs
deleted file mode 100644
index c00755ec2..000000000
--- a/Dalamud/Plugin/Internal/PluginRepository.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading.Tasks;
-
-using Dalamud.Logging.Internal;
-using Dalamud.Plugin.Internal.Types;
-using Newtonsoft.Json;
-
-namespace Dalamud.Plugin.Internal
-{
- ///
- /// This class represents a single plugin repository.
- ///
- internal class PluginRepository
- {
- private const string DalamudPluginsMasterUrl = "https://kamori.goats.dev/Plugin/PluginMaster";
-
- private static readonly ModuleLog Log = new("PLUGINR");
-
- private static readonly HttpClient HttpClient = new()
- {
- DefaultRequestHeaders =
- {
- CacheControl = new CacheControlHeaderValue
- {
- NoCache = true,
- },
- },
- };
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The plugin master URL.
- /// Whether the plugin repo is enabled.
- public PluginRepository(string pluginMasterUrl, bool isEnabled)
- {
- this.PluginMasterUrl = pluginMasterUrl;
- this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl;
- this.IsEnabled = isEnabled;
- }
-
- ///
- /// Gets a new instance of the class for the main repo.
- ///
- public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true);
-
- ///
- /// Gets the pluginmaster.json URL.
- ///
- public string PluginMasterUrl { get; }
-
- ///
- /// Gets a value indicating whether this plugin repository is from a third party.
- ///
- public bool IsThirdParty { get; }
-
- ///
- /// Gets a value indicating whether this repo is enabled.
- ///
- public bool IsEnabled { get; }
-
- ///
- /// Gets the plugin master list of available plugins.
- ///
- public ReadOnlyCollection? PluginMaster { get; private set; }
-
- ///
- /// Gets the initialization state of the plugin repository.
- ///
- public PluginRepositoryState State { get; private set; }
-
- ///
- /// Reload the plugin master asynchronously in a task.
- ///
- /// The new state.
- public async Task ReloadPluginMasterAsync()
- {
- this.State = PluginRepositoryState.InProgress;
- this.PluginMaster = new List().AsReadOnly();
-
- try
- {
- Log.Information($"Fetching repo: {this.PluginMasterUrl}");
-
- using var response = await HttpClient.GetAsync(this.PluginMasterUrl);
- response.EnsureSuccessStatusCode();
-
- var data = await response.Content.ReadAsStringAsync();
- var pluginMaster = JsonConvert.DeserializeObject>(data);
-
- if (pluginMaster == null)
- {
- throw new Exception("Deserialized PluginMaster was null.");
- }
-
- pluginMaster.Sort((pm1, pm2) => pm1.Name.CompareTo(pm2.Name));
-
- // Set the source for each remote manifest. Allows for checking if is 3rd party.
- foreach (var manifest in pluginMaster)
- {
- manifest.SourceRepo = this;
- }
-
- this.PluginMaster = pluginMaster.AsReadOnly();
-
- Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}");
- this.State = PluginRepositoryState.Success;
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"PluginMaster failed: {this.PluginMasterUrl}");
- this.State = PluginRepositoryState.Fail;
- }
- }
- }
-}
diff --git a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
index 32dde337c..13523a379 100644
--- a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
+++ b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
@@ -1,36 +1,35 @@
-namespace Dalamud.Plugin.Internal.Types
+namespace Dalamud.Plugin.Internal.Types;
+
+///
+/// Information about an available plugin update.
+///
+internal record AvailablePluginUpdate
{
///
- /// Information about an available plugin update.
+ /// Initializes a new instance of the class.
///
- internal record AvailablePluginUpdate
+ /// The installed plugin to update.
+ /// The manifest to use for the update.
+ /// If the testing version should be used for the update.
+ public AvailablePluginUpdate(LocalPlugin installedPlugin, RemotePluginManifest updateManifest, bool useTesting)
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The installed plugin to update.
- /// The manifest to use for the update.
- /// If the testing version should be used for the update.
- public AvailablePluginUpdate(LocalPlugin installedPlugin, RemotePluginManifest updateManifest, bool useTesting)
- {
- this.InstalledPlugin = installedPlugin;
- this.UpdateManifest = updateManifest;
- this.UseTesting = useTesting;
- }
-
- ///
- /// Gets the currently installed plugin.
- ///
- public LocalPlugin InstalledPlugin { get; init; }
-
- ///
- /// Gets the available update manifest.
- ///
- public RemotePluginManifest UpdateManifest { get; init; }
-
- ///
- /// Gets a value indicating whether the update should use the testing URL.
- ///
- public bool UseTesting { get; init; }
+ this.InstalledPlugin = installedPlugin;
+ this.UpdateManifest = updateManifest;
+ this.UseTesting = useTesting;
}
+
+ ///
+ /// Gets the currently installed plugin.
+ ///
+ public LocalPlugin InstalledPlugin { get; init; }
+
+ ///
+ /// Gets the available update manifest.
+ ///
+ public RemotePluginManifest UpdateManifest { get; init; }
+
+ ///
+ /// Gets a value indicating whether the update should use the testing URL.
+ ///
+ public bool UseTesting { get; init; }
}
diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
new file mode 100644
index 000000000..eb0877227
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
@@ -0,0 +1,162 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Interface.Internal.Notifications;
+using Dalamud.Logging.Internal;
+
+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.
+///
+internal class LocalDevPlugin : LocalPlugin, IDisposable
+{
+ private static readonly ModuleLog Log = new("PLUGIN");
+
+ // Ref to Dalamud.Configuration.DevPluginSettings
+ private readonly DevPluginSettings devSettings;
+
+ private FileSystemWatcher? fileWatcher;
+ private CancellationTokenSource fileWatcherTokenSource = new();
+ private int reloadCounter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Path to the DLL file.
+ /// The plugin manifest.
+ public LocalDevPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
+ : base(dllFile, manifest)
+ {
+ var configuration = Service.Get();
+
+ if (!configuration.DevPluginSettings.TryGetValue(dllFile.FullName, out this.devSettings))
+ {
+ configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings();
+ configuration.Save();
+ }
+
+ if (this.AutomaticReload)
+ {
+ this.EnableReloading();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this dev plugin should start on boot.
+ ///
+ public bool StartOnBoot
+ {
+ get => this.devSettings.StartOnBoot;
+ set => this.devSettings.StartOnBoot = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this dev plugin should reload on change.
+ ///
+ public bool AutomaticReload
+ {
+ get => this.devSettings.AutomaticReloading;
+ set
+ {
+ this.devSettings.AutomaticReloading = value;
+
+ if (this.devSettings.AutomaticReloading)
+ {
+ this.EnableReloading();
+ }
+ else
+ {
+ this.DisableReloading();
+ }
+ }
+ }
+
+ ///
+ public new void Dispose()
+ {
+ if (this.fileWatcher != null)
+ {
+ this.fileWatcher.Changed -= this.OnFileChanged;
+ this.fileWatcherTokenSource.Cancel();
+ this.fileWatcher.Dispose();
+ }
+
+ base.Dispose();
+ }
+
+ ///
+ /// Configure this plugin for automatic reloading and enable it.
+ ///
+ public void EnableReloading()
+ {
+ if (this.fileWatcher == null && this.DllFile.DirectoryName != null)
+ {
+ this.fileWatcherTokenSource = new CancellationTokenSource();
+ this.fileWatcher = new FileSystemWatcher(this.DllFile.DirectoryName);
+ this.fileWatcher.Changed += this.OnFileChanged;
+ this.fileWatcher.Filter = this.DllFile.Name;
+ this.fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
+ this.fileWatcher.EnableRaisingEvents = true;
+ }
+ }
+
+ ///
+ /// Disable automatic reloading for this plugin.
+ ///
+ public void DisableReloading()
+ {
+ if (this.fileWatcher != null)
+ {
+ this.fileWatcherTokenSource.Cancel();
+ this.fileWatcher.Changed -= this.OnFileChanged;
+ this.fileWatcher.Dispose();
+ this.fileWatcher = null;
+ }
+ }
+
+ private void OnFileChanged(object sender, FileSystemEventArgs args)
+ {
+ var current = Interlocked.Increment(ref this.reloadCounter);
+
+ Task.Delay(500).ContinueWith(
+ _ =>
+ {
+ if (this.fileWatcherTokenSource.IsCancellationRequested)
+ {
+ Log.Debug($"Skipping reload of {this.Name}, file watcher was cancelled.");
+ return;
+ }
+
+ if (current != this.reloadCounter)
+ {
+ Log.Debug($"Skipping reload of {this.Name}, file has changed again.");
+ return;
+ }
+
+ if (this.State != PluginState.Loaded)
+ {
+ Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded}.");
+ return;
+ }
+
+ var notificationManager = Service.Get();
+
+ try
+ {
+ this.Reload();
+ notificationManager.AddNotification($"The DevPlugin '{this.Name} was reloaded successfully.", "Plugin reloaded!", NotificationType.Success);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "DevPlugin reload failed.");
+ notificationManager.AddNotification($"The DevPlugin '{this.Name} could not be reloaded.", "Plugin reload failed!", NotificationType.Error);
+ }
+ },
+ this.fileWatcherTokenSource.Token);
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
new file mode 100644
index 000000000..1ac413ca1
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
@@ -0,0 +1,501 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Game;
+using Dalamud.Game.Gui.Dtr;
+using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Internal.Exceptions;
+using Dalamud.Plugin.Internal.Loader;
+using Dalamud.Utility;
+using Dalamud.Utility.Signatures;
+
+namespace Dalamud.Plugin.Internal.Types;
+
+///
+/// This class represents a plugin and all facets of its lifecycle.
+/// The DLL on disk, dependencies, loaded assembly, etc.
+///
+internal class LocalPlugin : IDisposable
+{
+ private static readonly ModuleLog Log = new("LOCALPLUGIN");
+
+ private readonly FileInfo manifestFile;
+ private readonly FileInfo disabledFile;
+ private readonly FileInfo testingFile;
+
+ private PluginLoader? loader;
+ private Assembly? pluginAssembly;
+ private Type? pluginType;
+ private IDalamudPlugin? instance;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Path to the DLL file.
+ /// The plugin manifest.
+ public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
+ {
+ if (dllFile.Name == "FFXIVClientStructs.Generators.dll")
+ {
+ // Could this be done another way? Sure. It is an extremely common source
+ // of errors in the log through, and should never be loaded as a plugin.
+ Log.Error($"Not a plugin: {dllFile.FullName}");
+ throw new InvalidPluginException(dllFile);
+ }
+
+ this.DllFile = dllFile;
+ this.State = PluginState.Unloaded;
+
+ this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);
+
+ try
+ {
+ this.pluginAssembly = this.loader.LoadDefaultAssembly();
+ }
+ catch (Exception ex)
+ {
+ this.pluginAssembly = null;
+ this.pluginType = null;
+ this.loader.Dispose();
+
+ Log.Error(ex, $"Not a plugin: {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)));
+ }
+
+ if (this.pluginType == default)
+ {
+ this.pluginAssembly = null;
+ this.pluginType = null;
+ this.loader.Dispose();
+
+ Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}");
+ throw new InvalidPluginException(this.DllFile);
+ }
+
+ var assemblyVersion = this.pluginAssembly.GetName().Version;
+
+ // Although it is conditionally used here, we need to set the initial value regardless.
+ this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
+
+ // If the parameter manifest was null
+ if (manifest == null)
+ {
+ this.Manifest = new LocalPluginManifest()
+ {
+ Author = "developer",
+ Name = Path.GetFileNameWithoutExtension(this.DllFile.Name),
+ InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name),
+ AssemblyVersion = assemblyVersion ?? new Version("1.0.0.0"),
+ Description = string.Empty,
+ ApplicableVersion = GameVersion.Any,
+ DalamudApiLevel = PluginManager.DalamudApiLevel,
+ IsHide = false,
+ };
+
+ // Save the manifest to disk so there won't be any problems later.
+ // We'll update the name property after it can be retrieved from the instance.
+ this.Manifest.Save(this.manifestFile);
+ }
+ else
+ {
+ this.Manifest = manifest;
+ }
+
+ // This converts from the ".disabled" file feature to the manifest instead.
+ this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
+ if (this.disabledFile.Exists)
+ {
+ this.Manifest.Disabled = true;
+ this.disabledFile.Delete();
+ }
+
+ // This converts from the ".testing" file feature to the manifest instead.
+ this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile);
+ if (this.testingFile.Exists)
+ {
+ this.Manifest.Testing = true;
+ this.testingFile.Delete();
+ }
+
+ var pluginManager = Service.Get();
+ this.IsBanned = pluginManager.IsManifestBanned(this.Manifest);
+ this.BanReason = pluginManager.GetBanReason(this.Manifest);
+
+ this.SaveManifest();
+ }
+
+ ///
+ /// Gets the associated with this plugin.
+ ///
+ public DalamudPluginInterface? DalamudInterface { get; private set; }
+
+ ///
+ /// Gets the path to the plugin DLL.
+ ///
+ public FileInfo DllFile { get; }
+
+ ///
+ /// Gets the plugin manifest, if one exists.
+ ///
+ public LocalPluginManifest Manifest { get; private set; }
+
+ ///
+ /// Gets or sets the current state of the plugin.
+ ///
+ public PluginState State { get; protected set; }
+
+ ///
+ /// Gets the AssemblyName plugin, populated during .
+ ///
+ /// Plugin type.
+ public AssemblyName? AssemblyName { get; private set; }
+
+ ///
+ /// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
+ ///
+ public string Name => this.instance?.Name ?? this.Manifest.Name;
+
+ ///
+ /// Gets an optional reason, if the plugin is banned.
+ ///
+ public string BanReason { get; }
+
+ ///
+ /// Gets a value indicating whether the plugin is loaded and running.
+ ///
+ public bool IsLoaded => this.State == PluginState.Loaded;
+
+ ///
+ /// Gets a value indicating whether the plugin is disabled.
+ ///
+ public bool IsDisabled => this.Manifest.Disabled;
+
+ ///
+ /// Gets a value indicating whether this plugin's API level is out of date.
+ ///
+ public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
+
+ ///
+ /// Gets a value indicating whether the plugin is for testing use only.
+ ///
+ public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
+
+ ///
+ /// Gets a value indicating whether this plugin has been banned.
+ ///
+ public bool IsBanned { get; }
+
+ ///
+ /// Gets a value indicating whether this plugin is dev plugin.
+ ///
+ public bool IsDev => this is LocalDevPlugin;
+
+ ///
+ public void Dispose()
+ {
+ this.instance?.Dispose();
+ this.instance = null;
+
+ this.DalamudInterface?.ExplicitDispose();
+ this.DalamudInterface = null;
+
+ this.pluginType = null;
+ this.pluginAssembly = null;
+
+ this.loader?.Dispose();
+ }
+
+ ///
+ /// Load this plugin.
+ ///
+ /// The reason why this plugin is being loaded.
+ /// Load while reloading.
+ public void Load(PluginLoadReason reason, bool reloading = false)
+ {
+ var startInfo = Service.Get();
+ var configuration = Service.Get();
+ var pluginManager = Service.Get();
+
+ // Allowed: Unloaded
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working");
+ case PluginState.Loaded:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded");
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first");
+ case PluginState.UnloadError:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
+ case PluginState.Unloaded:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(this.State.ToString());
+ }
+
+ if (pluginManager.IsManifestBanned(this.Manifest))
+ throw new BannedPluginException($"Unable to load {this.Name}, banned");
+
+ if (this.Manifest.ApplicableVersion < startInfo.GameVersion)
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
+
+ if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.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");
+
+ this.State = PluginState.InProgress;
+ Log.Information($"Loading {this.DllFile.Name}");
+
+ if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll")))
+ {
+ Log.Error("==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", this.Manifest.Author!, this.Manifest.InternalName);
+ Log.Error("YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!");
+ Log.Error("You may not be able to load your plugin. \"False\" needs to be set in your csproj.");
+ Log.Error("If you are using ILMerge, do not merge anything other than your direct dependencies.");
+ Log.Error("Do not merge FFXIVClientStructs.Generators.dll.");
+ Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
+ }
+
+ try
+ {
+ this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);
+
+ if (reloading || this.IsDev)
+ {
+ if (this.IsDev)
+ {
+ // If a dev plugin is set to not autoload on boot, but we want to reload it at the arbitrary load
+ // time, we need to essentially "Unload" the plugin, but we can't call plugin.Unload because of the
+ // load state checks. Null any references to the assembly and types, then proceed with regular reload
+ // operations.
+ this.pluginAssembly = null;
+ this.pluginType = null;
+ }
+
+ this.loader.Reload();
+
+ if (this.IsDev)
+ {
+ // Reload the manifest in-case there were changes here too.
+ var manifestDevFile = LocalPluginManifest.GetManifestFile(this.DllFile);
+ if (manifestDevFile.Exists)
+ {
+ this.Manifest = LocalPluginManifest.Load(manifestDevFile);
+ }
+ }
+ }
+
+ // 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)));
+
+ // Check for any loaded plugins with the same assembly 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
+ if (otherPlugin == this || otherPlugin.instance == null)
+ continue;
+
+ var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name;
+ if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null)
+ {
+ this.State = PluginState.Unloaded;
+ Log.Debug($"Duplicate assembly: {this.Name}");
+
+ throw new DuplicatePluginException(assemblyName);
+ }
+ }
+
+ // Update the location for the Location and CodeBase patches
+ PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
+
+ this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev);
+
+ var ioc = Service.Get();
+ this.instance = ioc.Create(this.pluginType, this.DalamudInterface) as IDalamudPlugin;
+ if (this.instance == null)
+ {
+ this.State = PluginState.LoadError;
+ this.DalamudInterface.ExplicitDispose();
+ Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor");
+ return;
+ }
+
+ SignatureHelper.Initialise(this.instance);
+
+ // In-case the manifest name was a placeholder. Can occur when no manifest was included.
+ if (this.instance.Name != this.Manifest.Name)
+ {
+ this.Manifest.Name = this.instance.Name;
+ this.Manifest.Save(this.manifestFile);
+ }
+
+ this.State = PluginState.Loaded;
+ Log.Information($"Finished loading {this.DllFile.Name}");
+ }
+ catch (Exception ex)
+ {
+ this.State = PluginState.LoadError;
+ Log.Error(ex, $"Error while loading {this.Name}");
+
+ throw;
+ }
+ }
+
+ ///
+ /// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay
+ /// in the plugin list until it has been actually disposed.
+ ///
+ /// Unload while reloading.
+ public void Unload(bool reloading = false)
+ {
+ // Allowed: Loaded, LoadError(we are cleaning this up while we're at it)
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working");
+ case PluginState.Unloaded:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
+ case PluginState.UnloadError:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud");
+ case PluginState.Loaded:
+ break;
+ case PluginState.LoadError:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(this.State.ToString());
+ }
+
+ try
+ {
+ this.State = PluginState.InProgress;
+ Log.Information($"Unloading {this.DllFile.Name}");
+
+ this.instance?.Dispose();
+ this.instance = null;
+
+ this.DalamudInterface?.ExplicitDispose();
+ this.DalamudInterface = null;
+
+ this.pluginType = null;
+ this.pluginAssembly = null;
+
+ if (!reloading)
+ {
+ this.loader?.Dispose();
+ this.loader = null;
+ }
+
+ this.State = PluginState.Unloaded;
+ Log.Information($"Finished unloading {this.DllFile.Name}");
+ }
+ catch (Exception ex)
+ {
+ this.State = PluginState.UnloadError;
+ Log.Error(ex, $"Error while unloading {this.Name}");
+
+ throw;
+ }
+ }
+
+ ///
+ /// Reload this plugin.
+ ///
+ public void Reload()
+ {
+ this.Unload(true);
+
+ // 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.Get();
+ dtr.HandleRemovedNodes();
+
+ this.Load(PluginLoadReason.Reload, true);
+ }
+
+ ///
+ /// Revert a disable. Must be unloaded first, does not load.
+ ///
+ public void Enable()
+ {
+ // Allowed: Unloaded, UnloadError
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ case PluginState.Loaded:
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
+ case PluginState.Unloaded:
+ break;
+ case PluginState.UnloadError:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(this.State.ToString());
+ }
+
+ if (!this.Manifest.Disabled)
+ throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
+
+ this.Manifest.Disabled = false;
+ this.SaveManifest();
+ }
+
+ ///
+ /// Disable this plugin, must be unloaded first.
+ ///
+ public void Disable()
+ {
+ // Allowed: Unloaded, UnloadError
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ case PluginState.Loaded:
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");
+ case PluginState.Unloaded:
+ break;
+ case PluginState.UnloadError:
+ break;
+ 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();
+ }
+
+ private static void SetupLoaderConfig(LoaderConfig config)
+ {
+ config.IsUnloadable = true;
+ config.LoadInMemory = true;
+ config.PreferSharedTypes = false;
+ config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName());
+ config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName());
+ }
+
+ private void SaveManifest() => this.Manifest.Save(this.manifestFile);
+}
diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs
index a68418b2f..261f28b0e 100644
--- a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs
+++ b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs
@@ -1,99 +1,79 @@
using System.IO;
-using Dalamud.Utility;
using Newtonsoft.Json;
-namespace Dalamud.Plugin.Internal.Types
+namespace Dalamud.Plugin.Internal.Types;
+
+///
+/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
+/// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
+///
+internal record LocalPluginManifest : PluginManifest
{
///
- /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
- /// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
+ /// 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.
///
- internal record LocalPluginManifest : PluginManifest
- {
- ///
- /// Gets or sets a value indicating whether the plugin is disabled and should not be loaded.
- /// This value supercedes the ".disabled" file functionality and should not be included in the plugin master.
- ///
- public bool Disabled { get; set; } = false;
+ public bool Disabled { get; set; }
- ///
- /// Gets or sets a value indicating whether the plugin should only be loaded when testing is enabled.
- /// This value supercedes the ".testing" file functionality and should not be included in the plugin master.
- ///
- public bool Testing { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether the plugin should only be loaded when testing is enabled.
+ /// This value supersedes the ".testing" file functionality and should not be included in the plugin master.
+ ///
+ public bool Testing { get; set; }
- ///
- /// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
- /// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null
- /// when installed from the main repo.
- ///
- public string InstalledFromUrl { get; set; }
+ ///
+ /// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
+ /// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null
+ /// when installed from the main repo.
+ ///
+ public string InstalledFromUrl { get; set; } = string.Empty;
- ///
- /// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
- /// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null.
- ///
- public bool IsThirdParty => !string.IsNullOrEmpty(this.InstalledFromUrl);
+ ///
+ /// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
+ /// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null.
+ ///
+ public bool IsThirdParty => !string.IsNullOrEmpty(this.InstalledFromUrl);
- ///
- /// Save a plugin manifest to file.
- ///
- /// Path to save at.
- public void Save(FileInfo manifestFile) => File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
+ ///
+ /// Save a plugin manifest to file.
+ ///
+ /// Path to save at.
+ public void Save(FileInfo manifestFile) => File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
- ///
- /// Loads a plugin manifest from file.
- ///
- /// Path to the manifest.
- /// A object.
- public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName));
+ ///
+ /// Loads a plugin manifest from file.
+ ///
+ /// Path to the manifest.
+ /// A object.
+ public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName))!;
- ///
- /// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist.
- ///
- /// Manifest directory.
- /// The manifest.
- /// The file.
- public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll"));
+ ///
+ /// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist.
+ ///
+ /// Manifest directory.
+ /// The manifest.
+ /// The file.
+ public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll"));
- ///
- /// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist.
- ///
- /// The plugin DLL.
- /// The file.
- public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json"));
+ ///
+ /// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist.
+ ///
+ /// The plugin DLL.
+ /// The file.
+ public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json"));
- ///
- /// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist.
- ///
- /// The plugin DLL.
- /// The .disabled file.
- public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".disabled"));
+ ///
+ /// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist.
+ ///
+ /// The plugin DLL.
+ /// The .disabled file.
+ public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, ".disabled"));
- ///
- /// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist.
- ///
- /// The plugin DLL.
- /// The .testing file.
- public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".testing"));
-
- ///
- /// Check if this manifest is valid.
- ///
- /// Whether or not this manifest is valid.
- public bool CheckSanity()
- {
- if (this.InternalName.IsNullOrEmpty())
- return false;
-
- if (this.Name.IsNullOrEmpty())
- return false;
-
- if (this.DalamudApiLevel == 0)
- return false;
-
- return true;
- }
- }
+ ///
+ /// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist.
+ ///
+ /// The plugin DLL.
+ /// The .testing file.
+ public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, ".testing"));
}
diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
index 9d28a1b5c..422c1b0c8 100644
--- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs
+++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
@@ -4,177 +4,159 @@ using System.Collections.Generic;
using Dalamud.Game;
using Newtonsoft.Json;
-namespace Dalamud.Plugin.Internal.Types
+namespace Dalamud.Plugin.Internal.Types;
+
+///
+/// Information about a plugin, packaged in a json file with the DLL.
+///
+internal record PluginManifest
{
///
- /// Information about a plugin, packaged in a json file with the DLL.
+ /// Gets the author/s of the plugin.
///
- internal record PluginManifest
- {
- ///
- /// Gets the author/s of the plugin.
- ///
- [JsonProperty]
- public string? Author { get; init; }
+ [JsonProperty]
+ public string? Author { get; init; }
- ///
- /// Gets or sets the public name of the plugin.
- ///
- [JsonProperty]
- public string Name { get; set; }
+ ///
+ /// Gets or sets the public name of the plugin.
+ ///
+ [JsonProperty]
+ public string Name { get; set; } = null!;
- ///
- /// Gets a punchline of the plugins functions.
- ///
- [JsonProperty]
- public string? Punchline { get; init; }
+ ///
+ /// Gets a punchline of the plugins functions.
+ ///
+ [JsonProperty]
+ public string? Punchline { get; init; }
- ///
- /// Gets a description of the plugins functions.
- ///
- [JsonProperty]
- public string? Description { get; init; }
+ ///
+ /// Gets a description of the plugins functions.
+ ///
+ [JsonProperty]
+ public string? Description { get; init; }
- ///
- /// Gets a changelog.
- ///
- [JsonProperty]
- public string? Changelog { get; init; }
+ ///
+ /// Gets a changelog.
+ ///
+ [JsonProperty]
+ public string? Changelog { get; init; }
- ///
- /// Gets a list of tags defined on the plugin.
- ///
- [JsonProperty]
- public List? Tags { get; init; }
+ ///
+ /// Gets a list of tags defined on the plugin.
+ ///
+ [JsonProperty]
+ public List? Tags { get; init; }
- ///
- /// Gets a list of category tags defined on the plugin.
- ///
- [JsonProperty]
- public List? CategoryTags { get; init; }
+ ///
+ /// Gets a list of category tags defined on the plugin.
+ ///
+ [JsonProperty]
+ public List? CategoryTags { get; init; }
- ///
- /// Gets a value indicating whether or not 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.
- ///
- [JsonProperty]
- public bool IsHide { get; init; }
+ ///
+ /// Gets a value indicating whether or not 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.
+ ///
+ [JsonProperty]
+ public bool IsHide { get; init; }
- ///
- /// Gets the internal name of the plugin, which should match the assembly name of the plugin.
- ///
- [JsonProperty]
- public string InternalName { get; init; }
+ ///
+ /// Gets the internal name of the plugin, which should match the assembly name of the plugin.
+ ///
+ [JsonProperty]
+ public string InternalName { get; init; } = null!;
- ///
- /// Gets the current assembly version of the plugin.
- ///
- [JsonProperty]
- public Version AssemblyVersion { get; init; }
+ ///
+ /// Gets the current assembly version of the plugin.
+ ///
+ [JsonProperty]
+ public Version AssemblyVersion { get; init; } = null!;
- ///
- /// Gets the current testing assembly version of the plugin.
- ///
- [JsonProperty]
- public Version? TestingAssemblyVersion { get; init; }
+ ///
+ /// Gets the current testing assembly version of the plugin.
+ ///
+ [JsonProperty]
+ public Version? TestingAssemblyVersion { get; init; }
- ///
- /// Gets a value indicating whether the is not null.
- ///
- [JsonIgnore]
- public bool HasAssemblyVersion => this.AssemblyVersion != null;
+ ///
+ /// Gets a value indicating whether the plugin is only available for testing.
+ ///
+ [JsonProperty]
+ public bool IsTestingExclusive { get; init; }
- ///
- /// Gets a value indicating whether the is not null.
- ///
- [JsonIgnore]
- public bool HasTestingAssemblyVersion => this.TestingAssemblyVersion != null;
+ ///
+ /// Gets an URL to the website or source code of the plugin.
+ ///
+ [JsonProperty]
+ public string? RepoUrl { get; init; }
- ///
- /// Gets a value indicating whether the plugin is only available for testing.
- ///
- [JsonProperty]
- public bool IsTestingExclusive { get; init; }
+ ///
+ /// Gets the version of the game this plugin works with.
+ ///
+ [JsonProperty]
+ [JsonConverter(typeof(GameVersionConverter))]
+ public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any;
- ///
- /// Gets an URL to the website or source code of the plugin.
- ///
- [JsonProperty]
- public string? RepoUrl { get; init; }
+ ///
+ /// Gets the API level of this plugin. For the current API level, please see
+ /// for the currently used API level.
+ ///
+ [JsonProperty]
+ public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel;
- ///
- /// Gets the version of the game this plugin works with.
- ///
- [JsonProperty]
- [JsonConverter(typeof(GameVersionConverter))]
- public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any;
+ ///
+ /// Gets the number of downloads this plugin has.
+ ///
+ [JsonProperty]
+ public long DownloadCount { get; init; }
- ///
- /// Gets the API level of this plugin. For the current API level, please see
- /// for the currently used API level.
- ///
- [JsonProperty]
- public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel;
+ ///
+ /// Gets the last time this plugin was updated.
+ ///
+ [JsonProperty]
+ public long LastUpdate { get; init; }
- ///
- /// Gets the number of downloads this plugin has.
- ///
- [JsonProperty]
- public long DownloadCount { get; init; }
+ ///
+ /// Gets the download link used to install the plugin.
+ ///
+ [JsonProperty]
+ public string DownloadLinkInstall { get; init; } = null!;
- ///
- /// Gets the last time this plugin was updated.
- ///
- [JsonProperty]
- public long LastUpdate { get; init; }
+ ///
+ /// Gets the download link used to update the plugin.
+ ///
+ [JsonProperty]
+ public string DownloadLinkUpdate { get; init; } = null!;
- ///
- /// Gets the download link used to install the plugin.
- ///
- [JsonProperty]
- public string DownloadLinkInstall { get; init; }
+ ///
+ /// Gets the download link used to get testing versions of the plugin.
+ ///
+ [JsonProperty]
+ public string DownloadLinkTesting { get; init; } = null!;
- ///
- /// Gets the download link used to update the plugin.
- ///
- [JsonProperty]
- public string DownloadLinkUpdate { get; init; }
+ ///
+ /// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
+ ///
+ [JsonProperty]
+ public int LoadPriority { get; init; }
- ///
- /// Gets the download link used to get testing versions of the plugin.
- ///
- [JsonProperty]
- public string DownloadLinkTesting { get; init; }
+ ///
+ /// Gets a list of screenshot image URLs to show in the plugin installer.
+ ///
+ public List? ImageUrls { get; init; }
- ///
- /// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
- ///
- [JsonProperty]
- public int LoadPriority { get; init; }
+ ///
+ /// Gets an URL for the plugin's icon.
+ ///
+ public string? IconUrl { get; init; }
- ///
- /// Gets a list of screenshot image URLs to show in the plugin installer.
- ///
- public List? ImageUrls { get; init; }
+ ///
+ /// Gets a value indicating whether this plugin accepts feedback.
+ ///
+ public bool AcceptsFeedback { get; init; } = true;
- ///
- /// Gets an URL for the plugin's icon.
- ///
- public string? IconUrl { get; init; }
-
- ///
- /// Gets a value indicating whether this plugin accepts feedback.
- ///
- public bool AcceptsFeedback { get; init; } = true;
-
- ///
- /// Gets a message that is shown to users when sending feedback.
- ///
- public string? FeedbackMessage { get; init; }
-
- ///
- /// Gets a value indicating the webhook URL feedback is sent to.
- ///
- public string? FeedbackWebhook { get; init; }
- }
+ ///
+ /// Gets a message that is shown to users when sending feedback.
+ ///
+ public string? FeedbackMessage { get; init; }
}
diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs
new file mode 100644
index 000000000..63ea5c5d4
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+
+using Dalamud.Logging.Internal;
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal.Types;
+
+///
+/// This class represents a single plugin repository.
+///
+internal class PluginRepository
+{
+ private const string DalamudPluginsMasterUrl = "https://kamori.goats.dev/Plugin/PluginMaster";
+
+ private static readonly ModuleLog Log = new("PLUGINR");
+
+ private static readonly HttpClient HttpClient = new()
+ {
+ DefaultRequestHeaders =
+ {
+ CacheControl = new CacheControlHeaderValue
+ {
+ NoCache = true,
+ },
+ },
+ };
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The plugin master URL.
+ /// Whether the plugin repo is enabled.
+ public PluginRepository(string pluginMasterUrl, bool isEnabled)
+ {
+ this.PluginMasterUrl = pluginMasterUrl;
+ this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl;
+ this.IsEnabled = isEnabled;
+ }
+
+ ///
+ /// Gets a new instance of the class for the main repo.
+ ///
+ public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true);
+
+ ///
+ /// Gets the pluginmaster.json URL.
+ ///
+ public string PluginMasterUrl { get; }
+
+ ///
+ /// Gets a value indicating whether this plugin repository is from a third party.
+ ///
+ public bool IsThirdParty { get; }
+
+ ///
+ /// Gets a value indicating whether this repo is enabled.
+ ///
+ public bool IsEnabled { get; }
+
+ ///
+ /// Gets the plugin master list of available plugins.
+ ///
+ public ReadOnlyCollection? PluginMaster { get; private set; }
+
+ ///
+ /// Gets the initialization state of the plugin repository.
+ ///
+ public PluginRepositoryState State { get; private set; }
+
+ ///
+ /// Reload the plugin master asynchronously in a task.
+ ///
+ /// The new state.
+ public async Task ReloadPluginMasterAsync()
+ {
+ this.State = PluginRepositoryState.InProgress;
+ this.PluginMaster = new List().AsReadOnly();
+
+ try
+ {
+ Log.Information($"Fetching repo: {this.PluginMasterUrl}");
+
+ using var response = await HttpClient.GetAsync(this.PluginMasterUrl);
+ response.EnsureSuccessStatusCode();
+
+ var data = await response.Content.ReadAsStringAsync();
+ var pluginMaster = JsonConvert.DeserializeObject>(data);
+
+ if (pluginMaster == null)
+ {
+ throw new Exception("Deserialized PluginMaster was null.");
+ }
+
+ pluginMaster.Sort((pm1, pm2) => string.Compare(pm1.Name, pm2.Name, StringComparison.Ordinal));
+
+ // Set the source for each remote manifest. Allows for checking if is 3rd party.
+ foreach (var manifest in pluginMaster)
+ {
+ manifest.SourceRepo = this;
+ }
+
+ this.PluginMaster = pluginMaster.AsReadOnly();
+
+ Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}");
+ this.State = PluginRepositoryState.Success;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"PluginMaster failed: {this.PluginMasterUrl}");
+ this.State = PluginRepositoryState.Fail;
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs
index 46aa2c351..2909ff981 100644
--- a/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs
+++ b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs
@@ -1,28 +1,27 @@
-namespace Dalamud.Plugin.Internal.Types
+namespace Dalamud.Plugin.Internal.Types;
+
+///
+/// Values representing plugin repository state.
+///
+internal enum PluginRepositoryState
{
///
- /// Values representing plugin repository state.
+ /// State is unknown.
///
- internal enum PluginRepositoryState
- {
- ///
- /// State is unknown.
- ///
- Unknown,
+ Unknown,
- ///
- /// Currently loading.
- ///
- InProgress,
+ ///
+ /// Currently loading.
+ ///
+ InProgress,
- ///
- /// Load was successful.
- ///
- Success,
+ ///
+ /// Load was successful.
+ ///
+ Success,
- ///
- /// Load failed.
- ///
- Fail,
- }
+ ///
+ /// Load failed.
+ ///
+ Fail,
}
diff --git a/Dalamud/Plugin/Internal/Types/PluginState.cs b/Dalamud/Plugin/Internal/Types/PluginState.cs
index f32543b39..da5fcf977 100644
--- a/Dalamud/Plugin/Internal/Types/PluginState.cs
+++ b/Dalamud/Plugin/Internal/Types/PluginState.cs
@@ -1,33 +1,32 @@
-namespace Dalamud.Plugin.Internal.Types
+namespace Dalamud.Plugin.Internal.Types;
+
+///
+/// Values representing plugin load state.
+///
+internal enum PluginState
{
///
- /// Values representing plugin load state.
+ /// Plugin is defined, but unloaded.
///
- internal enum PluginState
- {
- ///
- /// Plugin is defined, but unloaded.
- ///
- Unloaded,
+ Unloaded,
- ///
- /// Plugin has thrown an error during unload.
- ///
- UnloadError,
+ ///
+ /// Plugin has thrown an error during unload.
+ ///
+ UnloadError,
- ///
- /// Currently loading.
- ///
- InProgress,
+ ///
+ /// Currently loading.
+ ///
+ InProgress,
- ///
- /// Load is successful.
- ///
- Loaded,
+ ///
+ /// Load is successful.
+ ///
+ Loaded,
- ///
- /// Plugin has thrown an error during loading.
- ///
- LoadError,
- }
+ ///
+ /// Plugin has thrown an error during loading.
+ ///
+ LoadError,
}
diff --git a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs
index f0394b9b7..02eba7ea7 100644
--- a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs
+++ b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs
@@ -1,30 +1,29 @@
using System;
-namespace Dalamud.Plugin.Internal.Types
+namespace Dalamud.Plugin.Internal.Types;
+
+///
+/// Plugin update status.
+///
+internal class PluginUpdateStatus
{
///
- /// Plugin update status.
+ /// Gets the plugin internal name.
///
- internal class PluginUpdateStatus
- {
- ///
- /// Gets or sets the plugin internal name.
- ///
- public string InternalName { get; set; }
+ public string InternalName { get; init; } = null!;
- ///
- /// Gets or sets the plugin name.
- ///
- public string Name { get; set; }
+ ///
+ /// Gets the plugin name.
+ ///
+ public string Name { get; init; } = null!;
- ///
- /// Gets or sets the plugin version.
- ///
- public Version Version { get; set; }
+ ///
+ /// Gets the plugin version.
+ ///
+ public Version Version { get; init; } = null!;
- ///
- /// Gets or sets a value indicating whether the plugin was updated.
- ///
- public bool WasUpdated { get; set; }
- }
+ ///
+ /// Gets or sets a value indicating whether the plugin was updated.
+ ///
+ public bool WasUpdated { get; set; }
}
diff --git a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs
index cbb989159..09084d569 100644
--- a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs
+++ b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs
@@ -1,18 +1,19 @@
+using JetBrains.Annotations;
using Newtonsoft.Json;
-namespace Dalamud.Plugin.Internal.Types
+namespace Dalamud.Plugin.Internal.Types;
+
+///
+/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
+/// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
+///
+[UsedImplicitly]
+internal record RemotePluginManifest : PluginManifest
{
///
- /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
- /// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
+ /// Gets or sets the plugin repository this manifest came from. Used in reporting which third party repo a manifest
+ /// may have come from in the plugins available view. This functionality should not be included in the plugin master.
///
- internal record RemotePluginManifest : PluginManifest
- {
- ///
- /// Gets or sets the plugin repository this manifest came from. Used in reporting which third party repo a manifest
- /// may have come from in the plugins available view. This functionality should not be included in the plugin master.
- ///
- [JsonIgnore]
- public PluginRepository SourceRepo { get; set; } = null;
- }
+ [JsonIgnore]
+ public PluginRepository SourceRepo { get; set; } = null!;
}