mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
refactor: fix plugin internal style errors (#830)
This commit is contained in:
parent
c2b43ad4ce
commit
8b7f9b58bf
17 changed files with 2076 additions and 2095 deletions
|
|
@ -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}]
|
||||
|
|
|
|||
|
|
@ -50,11 +50,15 @@
|
|||
<s:Boolean x:Key="/Default/UserDictionary/Words/=bannedplugin/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=clientopcode/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dalamud/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=FFXIV/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flytext/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gpose/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=LOCALPLUGIN/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=lumina/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Materia/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=PLUGINM/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=pluginmaster/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=PLUGINR/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Refilter/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=serveropcode/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Universalis/@EntryIndexedValue">True</s:Boolean>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.PluginInstaller
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a dev plugin and all facets of its lifecycle.
|
||||
/// The DLL on disk, dependencies, loaded assembly, etc.
|
||||
/// </summary>
|
||||
internal class LocalDevPlugin : LocalPlugin, 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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalDevPlugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dllFile">Path to the DLL file.</param>
|
||||
/// <param name="manifest">The plugin manifest.</param>
|
||||
public LocalDevPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
|
||||
: base(dllFile, manifest)
|
||||
{
|
||||
var configuration = Service<DalamudConfiguration>.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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this dev plugin should start on boot.
|
||||
/// </summary>
|
||||
public bool StartOnBoot
|
||||
{
|
||||
get => this.devSettings.StartOnBoot;
|
||||
set => this.devSettings.StartOnBoot = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this dev plugin should reload on change.
|
||||
/// </summary>
|
||||
public bool AutomaticReload
|
||||
{
|
||||
get => this.devSettings.AutomaticReloading;
|
||||
set
|
||||
{
|
||||
this.devSettings.AutomaticReloading = value;
|
||||
|
||||
if (this.devSettings.AutomaticReloading)
|
||||
{
|
||||
this.EnableReloading();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.DisableReloading();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public new void Dispose()
|
||||
{
|
||||
if (this.fileWatcher != null)
|
||||
{
|
||||
this.fileWatcher.Changed -= this.OnFileChanged;
|
||||
this.fileWatcherTokenSource.Cancel();
|
||||
this.fileWatcher.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure this plugin for automatic reloading and enable it.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable automatic reloading for this plugin.
|
||||
/// </summary>
|
||||
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<NotificationManager>.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a plugin and all facets of its lifecycle.
|
||||
/// The DLL on disk, dependencies, loaded assembly, etc.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dllFile">Path to the DLL file.</param>
|
||||
/// <param name="manifest">The plugin manifest.</param>
|
||||
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<PluginManager>.Get();
|
||||
this.IsBanned = pluginManager.IsManifestBanned(this.Manifest);
|
||||
this.BanReason = pluginManager.GetBanReason(this.Manifest);
|
||||
|
||||
this.SaveManifest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="DalamudPluginInterface"/> associated with this plugin.
|
||||
/// </summary>
|
||||
public DalamudPluginInterface? DalamudInterface { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin DLL.
|
||||
/// </summary>
|
||||
public FileInfo DllFile { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin manifest, if one exists.
|
||||
/// </summary>
|
||||
public LocalPluginManifest Manifest { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current state of the plugin.
|
||||
/// </summary>
|
||||
public PluginState State { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AssemblyName plugin, populated during <see cref="Load(PluginLoadReason, bool)"/>.
|
||||
/// </summary>
|
||||
/// <returns>Plugin type.</returns>
|
||||
public AssemblyName? AssemblyName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
|
||||
/// </summary>
|
||||
public string Name => this.instance?.Name ?? this.Manifest.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional reason, if the plugin is banned.
|
||||
/// </summary>
|
||||
public string BanReason { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is loaded and running.
|
||||
/// </summary>
|
||||
public bool IsLoaded => this.State == PluginState.Loaded;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is disabled.
|
||||
/// </summary>
|
||||
public bool IsDisabled => this.Manifest.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin's API level is out of date.
|
||||
/// </summary>
|
||||
public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is for testing use only.
|
||||
/// </summary>
|
||||
public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin has been banned.
|
||||
/// </summary>
|
||||
public bool IsBanned { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin is dev plugin.
|
||||
/// </summary>
|
||||
public bool IsDev => this is LocalDevPlugin;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.instance?.Dispose();
|
||||
this.instance = null;
|
||||
|
||||
this.DalamudInterface?.ExplicitDispose();
|
||||
this.DalamudInterface = null;
|
||||
|
||||
this.pluginType = null;
|
||||
this.pluginAssembly = null;
|
||||
|
||||
this.loader?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load this plugin.
|
||||
/// </summary>
|
||||
/// <param name="reason">The reason why this plugin is being loaded.</param>
|
||||
/// <param name="reloading">Load while reloading.</param>
|
||||
public void Load(PluginLoadReason reason, bool reloading = false)
|
||||
{
|
||||
var startInfo = Service<DalamudStartInfo>.Get();
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
var pluginManager = Service<PluginManager>.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. \"<Private>False</Private>\" 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<ServiceContainer>.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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="reloading">Unload while reloading.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload this plugin.
|
||||
/// </summary>
|
||||
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<DtrBar>.Get();
|
||||
dtr.HandleRemovedNodes();
|
||||
|
||||
this.Load(PluginLoadReason.Reload, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revert a disable. Must be unloaded first, does not load.
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
// Allowed: Unloaded, UnloadError
|
||||
switch (this.State)
|
||||
{
|
||||
case PluginState.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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable this plugin, must be unloaded first.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,13 +21,13 @@ using Dalamud.Plugin.Internal.Types;
|
|||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Plugin.Internal
|
||||
namespace Dalamud.Plugin.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for loading and unloading plugins.
|
||||
/// </summary>
|
||||
internal partial class PluginManager : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Class responsible for loading and unloading plugins.
|
||||
/// </summary>
|
||||
internal partial class PluginManager : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded.
|
||||
/// </summary>
|
||||
|
|
@ -1025,14 +1025,14 @@ namespace Dalamud.Plugin.Internal
|
|||
|
||||
public static string DalamudPluginUpdateFailed(string name, Version version) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed.").Format(name, version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for loading and unloading plugins.
|
||||
/// This contains the assembly patching functionality to resolve assembly locations.
|
||||
/// </summary>
|
||||
internal partial class PluginManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Class responsible for loading and unloading plugins.
|
||||
/// This contains the assembly patching functionality to resolve assembly locations.
|
||||
/// </summary>
|
||||
internal partial class PluginManager
|
||||
{
|
||||
/// <summary>
|
||||
/// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading
|
||||
/// plugins via byte[].
|
||||
|
|
@ -1125,13 +1125,12 @@ namespace Dalamud.Plugin.Internal
|
|||
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
|
||||
#pragma warning disable CS0618
|
||||
#pragma warning disable SYSLIB0012
|
||||
var codebaseTarget = targetType.GetProperty(nameof(Assembly.CodeBase))?.GetGetMethod();
|
||||
#pragma warning restore SYSLIB0012
|
||||
#pragma warning restore CS0618
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a single plugin repository.
|
||||
/// </summary>
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="pluginMasterUrl">The plugin master URL.</param>
|
||||
/// <param name="isEnabled">Whether the plugin repo is enabled.</param>
|
||||
public PluginRepository(string pluginMasterUrl, bool isEnabled)
|
||||
{
|
||||
this.PluginMasterUrl = pluginMasterUrl;
|
||||
this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl;
|
||||
this.IsEnabled = isEnabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a new instance of the <see cref="PluginRepository"/> class for the main repo.
|
||||
/// </summary>
|
||||
public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pluginmaster.json URL.
|
||||
/// </summary>
|
||||
public string PluginMasterUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin repository is from a third party.
|
||||
/// </summary>
|
||||
public bool IsThirdParty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this repo is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin master list of available plugins.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<RemotePluginManifest>? PluginMaster { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initialization state of the plugin repository.
|
||||
/// </summary>
|
||||
public PluginRepositoryState State { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reload the plugin master asynchronously in a task.
|
||||
/// </summary>
|
||||
/// <returns>The new state.</returns>
|
||||
public async Task ReloadPluginMasterAsync()
|
||||
{
|
||||
this.State = PluginRepositoryState.InProgress;
|
||||
this.PluginMaster = new List<RemotePluginManifest>().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<List<RemotePluginManifest>>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
namespace Dalamud.Plugin.Internal.Types
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Information about an available plugin update.
|
||||
/// </summary>
|
||||
internal record AvailablePluginUpdate
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about an available plugin update.
|
||||
/// </summary>
|
||||
internal record AvailablePluginUpdate
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AvailablePluginUpdate"/> class.
|
||||
/// </summary>
|
||||
|
|
@ -32,5 +32,4 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
/// Gets a value indicating whether the update should use the testing URL.
|
||||
/// </summary>
|
||||
public bool UseTesting { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
162
Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
Normal file
162
Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a dev plugin and all facets of its lifecycle.
|
||||
/// The DLL on disk, dependencies, loaded assembly, etc.
|
||||
/// </summary>
|
||||
internal class LocalDevPlugin : LocalPlugin, 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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalDevPlugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dllFile">Path to the DLL file.</param>
|
||||
/// <param name="manifest">The plugin manifest.</param>
|
||||
public LocalDevPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
|
||||
: base(dllFile, manifest)
|
||||
{
|
||||
var configuration = Service<DalamudConfiguration>.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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this dev plugin should start on boot.
|
||||
/// </summary>
|
||||
public bool StartOnBoot
|
||||
{
|
||||
get => this.devSettings.StartOnBoot;
|
||||
set => this.devSettings.StartOnBoot = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this dev plugin should reload on change.
|
||||
/// </summary>
|
||||
public bool AutomaticReload
|
||||
{
|
||||
get => this.devSettings.AutomaticReloading;
|
||||
set
|
||||
{
|
||||
this.devSettings.AutomaticReloading = value;
|
||||
|
||||
if (this.devSettings.AutomaticReloading)
|
||||
{
|
||||
this.EnableReloading();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.DisableReloading();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public new void Dispose()
|
||||
{
|
||||
if (this.fileWatcher != null)
|
||||
{
|
||||
this.fileWatcher.Changed -= this.OnFileChanged;
|
||||
this.fileWatcherTokenSource.Cancel();
|
||||
this.fileWatcher.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure this plugin for automatic reloading and enable it.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable automatic reloading for this plugin.
|
||||
/// </summary>
|
||||
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<NotificationManager>.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);
|
||||
}
|
||||
}
|
||||
501
Dalamud/Plugin/Internal/Types/LocalPlugin.cs
Normal file
501
Dalamud/Plugin/Internal/Types/LocalPlugin.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a plugin and all facets of its lifecycle.
|
||||
/// The DLL on disk, dependencies, loaded assembly, etc.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dllFile">Path to the DLL file.</param>
|
||||
/// <param name="manifest">The plugin manifest.</param>
|
||||
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<PluginManager>.Get();
|
||||
this.IsBanned = pluginManager.IsManifestBanned(this.Manifest);
|
||||
this.BanReason = pluginManager.GetBanReason(this.Manifest);
|
||||
|
||||
this.SaveManifest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="DalamudPluginInterface"/> associated with this plugin.
|
||||
/// </summary>
|
||||
public DalamudPluginInterface? DalamudInterface { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin DLL.
|
||||
/// </summary>
|
||||
public FileInfo DllFile { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin manifest, if one exists.
|
||||
/// </summary>
|
||||
public LocalPluginManifest Manifest { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current state of the plugin.
|
||||
/// </summary>
|
||||
public PluginState State { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AssemblyName plugin, populated during <see cref="Load(PluginLoadReason, bool)"/>.
|
||||
/// </summary>
|
||||
/// <returns>Plugin type.</returns>
|
||||
public AssemblyName? AssemblyName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
|
||||
/// </summary>
|
||||
public string Name => this.instance?.Name ?? this.Manifest.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional reason, if the plugin is banned.
|
||||
/// </summary>
|
||||
public string BanReason { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is loaded and running.
|
||||
/// </summary>
|
||||
public bool IsLoaded => this.State == PluginState.Loaded;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is disabled.
|
||||
/// </summary>
|
||||
public bool IsDisabled => this.Manifest.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin's API level is out of date.
|
||||
/// </summary>
|
||||
public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is for testing use only.
|
||||
/// </summary>
|
||||
public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin has been banned.
|
||||
/// </summary>
|
||||
public bool IsBanned { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin is dev plugin.
|
||||
/// </summary>
|
||||
public bool IsDev => this is LocalDevPlugin;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.instance?.Dispose();
|
||||
this.instance = null;
|
||||
|
||||
this.DalamudInterface?.ExplicitDispose();
|
||||
this.DalamudInterface = null;
|
||||
|
||||
this.pluginType = null;
|
||||
this.pluginAssembly = null;
|
||||
|
||||
this.loader?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load this plugin.
|
||||
/// </summary>
|
||||
/// <param name="reason">The reason why this plugin is being loaded.</param>
|
||||
/// <param name="reloading">Load while reloading.</param>
|
||||
public void Load(PluginLoadReason reason, bool reloading = false)
|
||||
{
|
||||
var startInfo = Service<DalamudStartInfo>.Get();
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
var pluginManager = Service<PluginManager>.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. \"<Private>False</Private>\" 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<ServiceContainer>.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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="reloading">Unload while reloading.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload this plugin.
|
||||
/// </summary>
|
||||
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<DtrBar>.Get();
|
||||
dtr.HandleRemovedNodes();
|
||||
|
||||
this.Load(PluginLoadReason.Reload, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revert a disable. Must be unloaded first, does not load.
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
// Allowed: Unloaded, UnloadError
|
||||
switch (this.State)
|
||||
{
|
||||
case PluginState.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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable this plugin, must be unloaded first.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,34 +1,33 @@
|
|||
using System.IO;
|
||||
|
||||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Types
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal record LocalPluginManifest : PluginManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal record LocalPluginManifest : PluginManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// This value supersedes the ".disabled" file functionality and should not be included in the plugin master.
|
||||
/// </summary>
|
||||
public bool Disabled { get; set; } = false;
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// This value supersedes the ".testing" file functionality and should not be included in the plugin master.
|
||||
/// </summary>
|
||||
public bool Testing { get; set; } = false;
|
||||
public bool Testing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public string InstalledFromUrl { get; set; }
|
||||
public string InstalledFromUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
|
||||
|
|
@ -47,7 +46,7 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
/// </summary>
|
||||
/// <param name="manifestFile">Path to the manifest.</param>
|
||||
/// <returns>A <see cref="PluginManifest"/> object.</returns>
|
||||
public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject<LocalPluginManifest>(File.ReadAllText(manifestFile.FullName));
|
||||
public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject<LocalPluginManifest>(File.ReadAllText(manifestFile.FullName))!;
|
||||
|
||||
/// <summary>
|
||||
/// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist.
|
||||
|
|
@ -62,38 +61,19 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
/// </summary>
|
||||
/// <param name="dllFile">The plugin DLL.</param>
|
||||
/// <returns>The <see cref="PluginManifest"/> file.</returns>
|
||||
public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json"));
|
||||
public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json"));
|
||||
|
||||
/// <summary>
|
||||
/// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist.
|
||||
/// </summary>
|
||||
/// <param name="dllFile">The plugin DLL.</param>
|
||||
/// <returns>The <see cref="PluginManifest"/> .disabled file.</returns>
|
||||
public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".disabled"));
|
||||
public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, ".disabled"));
|
||||
|
||||
/// <summary>
|
||||
/// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist.
|
||||
/// </summary>
|
||||
/// <param name="dllFile">The plugin DLL.</param>
|
||||
/// <returns>The <see cref="PluginManifest"/> .testing file.</returns>
|
||||
public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".testing"));
|
||||
|
||||
/// <summary>
|
||||
/// Check if this manifest is valid.
|
||||
/// </summary>
|
||||
/// <returns>Whether or not this manifest is valid.</returns>
|
||||
public bool CheckSanity()
|
||||
{
|
||||
if (this.InternalName.IsNullOrEmpty())
|
||||
return false;
|
||||
|
||||
if (this.Name.IsNullOrEmpty())
|
||||
return false;
|
||||
|
||||
if (this.DalamudApiLevel == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, ".testing"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ using System.Collections.Generic;
|
|||
using Dalamud.Game;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Types
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Information about a plugin, packaged in a json file with the DLL.
|
||||
/// </summary>
|
||||
internal record PluginManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about a plugin, packaged in a json file with the DLL.
|
||||
/// </summary>
|
||||
internal record PluginManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the author/s of the plugin.
|
||||
/// </summary>
|
||||
|
|
@ -21,7 +21,7 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
/// Gets or sets the public name of the plugin.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a punchline of the plugins functions.
|
||||
|
|
@ -64,13 +64,13 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
/// Gets the internal name of the plugin, which should match the assembly name of the plugin.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public string InternalName { get; init; }
|
||||
public string InternalName { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current assembly version of the plugin.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public Version AssemblyVersion { get; init; }
|
||||
public Version AssemblyVersion { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current testing assembly version of the plugin.
|
||||
|
|
@ -78,18 +78,6 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
[JsonProperty]
|
||||
public Version? TestingAssemblyVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the <see cref="AssemblyVersion"/> is not null.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool HasAssemblyVersion => this.AssemblyVersion != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the <see cref="TestingAssemblyVersion"/> is not null.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool HasTestingAssemblyVersion => this.TestingAssemblyVersion != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is only available for testing.
|
||||
/// </summary>
|
||||
|
|
@ -132,19 +120,19 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
/// Gets the download link used to install the plugin.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public string DownloadLinkInstall { get; init; }
|
||||
public string DownloadLinkInstall { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the download link used to update the plugin.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public string DownloadLinkUpdate { get; init; }
|
||||
public string DownloadLinkUpdate { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the download link used to get testing versions of the plugin.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public string DownloadLinkTesting { get; init; }
|
||||
public string DownloadLinkTesting { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
|
||||
|
|
@ -171,10 +159,4 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
/// Gets a message that is shown to users when sending feedback.
|
||||
/// </summary>
|
||||
public string? FeedbackMessage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating the webhook URL feedback is sent to.
|
||||
/// </summary>
|
||||
public string? FeedbackWebhook { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
118
Dalamud/Plugin/Internal/Types/PluginRepository.cs
Normal file
118
Dalamud/Plugin/Internal/Types/PluginRepository.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a single plugin repository.
|
||||
/// </summary>
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="pluginMasterUrl">The plugin master URL.</param>
|
||||
/// <param name="isEnabled">Whether the plugin repo is enabled.</param>
|
||||
public PluginRepository(string pluginMasterUrl, bool isEnabled)
|
||||
{
|
||||
this.PluginMasterUrl = pluginMasterUrl;
|
||||
this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl;
|
||||
this.IsEnabled = isEnabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a new instance of the <see cref="PluginRepository"/> class for the main repo.
|
||||
/// </summary>
|
||||
public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pluginmaster.json URL.
|
||||
/// </summary>
|
||||
public string PluginMasterUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin repository is from a third party.
|
||||
/// </summary>
|
||||
public bool IsThirdParty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this repo is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin master list of available plugins.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<RemotePluginManifest>? PluginMaster { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initialization state of the plugin repository.
|
||||
/// </summary>
|
||||
public PluginRepositoryState State { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reload the plugin master asynchronously in a task.
|
||||
/// </summary>
|
||||
/// <returns>The new state.</returns>
|
||||
public async Task ReloadPluginMasterAsync()
|
||||
{
|
||||
this.State = PluginRepositoryState.InProgress;
|
||||
this.PluginMaster = new List<RemotePluginManifest>().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<List<RemotePluginManifest>>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
namespace Dalamud.Plugin.Internal.Types
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Values representing plugin repository state.
|
||||
/// </summary>
|
||||
internal enum PluginRepositoryState
|
||||
{
|
||||
/// <summary>
|
||||
/// Values representing plugin repository state.
|
||||
/// </summary>
|
||||
internal enum PluginRepositoryState
|
||||
{
|
||||
/// <summary>
|
||||
/// State is unknown.
|
||||
/// </summary>
|
||||
|
|
@ -24,5 +24,4 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
/// Load failed.
|
||||
/// </summary>
|
||||
Fail,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
namespace Dalamud.Plugin.Internal.Types
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Values representing plugin load state.
|
||||
/// </summary>
|
||||
internal enum PluginState
|
||||
{
|
||||
/// <summary>
|
||||
/// Values representing plugin load state.
|
||||
/// </summary>
|
||||
internal enum PluginState
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin is defined, but unloaded.
|
||||
/// </summary>
|
||||
|
|
@ -29,5 +29,4 @@ namespace Dalamud.Plugin.Internal.Types
|
|||
/// Plugin has thrown an error during loading.
|
||||
/// </summary>
|
||||
LoadError,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,29 @@
|
|||
using System;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Types
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin update status.
|
||||
/// </summary>
|
||||
internal class PluginUpdateStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin update status.
|
||||
/// Gets the plugin internal name.
|
||||
/// </summary>
|
||||
internal class PluginUpdateStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the plugin internal name.
|
||||
/// </summary>
|
||||
public string InternalName { get; set; }
|
||||
public string InternalName { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the plugin name.
|
||||
/// Gets the plugin name.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
public string Name { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the plugin version.
|
||||
/// Gets the plugin version.
|
||||
/// </summary>
|
||||
public Version Version { get; set; }
|
||||
public Version Version { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the plugin was updated.
|
||||
/// </summary>
|
||||
public bool WasUpdated { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Types
|
||||
namespace Dalamud.Plugin.Internal.Types;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
internal record RemotePluginManifest : PluginManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal record RemotePluginManifest : PluginManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public PluginRepository SourceRepo { get; set; } = null;
|
||||
}
|
||||
public PluginRepository SourceRepo { get; set; } = null!;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue