merge master into profiles

This commit is contained in:
goat 2023-05-28 16:06:05 +02:00
commit fe6196d0ad
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
57 changed files with 1118 additions and 287 deletions

View file

@ -10,6 +10,7 @@ using System.Reflection;
using Dalamud.Configuration;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.Sanitizer;
@ -32,33 +33,30 @@ namespace Dalamud.Plugin;
/// </summary>
public sealed class DalamudPluginInterface : IDisposable
{
private readonly string pluginName;
private readonly LocalPlugin plugin;
private readonly PluginConfigurations configs;
/// <summary>
/// Initializes a new instance of the <see cref="DalamudPluginInterface"/> class.
/// Set up the interface and populate all fields needed.
/// </summary>
/// <param name="pluginName">The internal name of the plugin.</param>
/// <param name="assemblyLocation">Location of the assembly.</param>
/// <param name="plugin">The plugin this interface belongs to.</param>
/// <param name="reason">The reason the plugin was loaded.</param>
/// <param name="isDev">A value indicating whether this is a dev plugin.</param>
/// <param name="manifest">The local manifest for this plugin.</param>
internal DalamudPluginInterface(string pluginName, FileInfo assemblyLocation, PluginLoadReason reason, bool isDev, LocalPluginManifest manifest)
internal DalamudPluginInterface(
LocalPlugin plugin,
PluginLoadReason reason)
{
this.plugin = plugin;
var configuration = Service<DalamudConfiguration>.Get();
var dataManager = Service<DataManager>.Get();
var localization = Service<Localization>.Get();
this.UiBuilder = new UiBuilder(pluginName);
this.UiBuilder = new UiBuilder(plugin.Name);
this.pluginName = pluginName;
this.AssemblyLocation = assemblyLocation;
this.configs = Service<PluginManager>.Get().PluginConfigs;
this.Reason = reason;
this.IsDev = isDev;
this.SourceRepository = isDev ? LocalPluginManifest.FlagDevPlugin : manifest.InstalledFromUrl;
this.IsTesting = manifest.Testing;
this.SourceRepository = this.IsDev ? LocalPluginManifest.FlagDevPlugin : plugin.Manifest.InstalledFromUrl;
this.IsTesting = plugin.IsTesting;
this.LoadTime = DateTime.Now;
this.LoadTimeUTC = DateTime.UtcNow;
@ -88,16 +86,33 @@ public sealed class DalamudPluginInterface : IDisposable
/// <param name="langCode">The new language code.</param>
public delegate void LanguageChangedDelegate(string langCode);
/// <summary>
/// Delegate for events that listen to changes to the list of active plugins.
/// </summary>
/// <param name="kind">What action caused this event to be fired.</param>
/// <param name="affectedThisPlugin">If this plugin was affected by the change.</param>
public delegate void ActivePluginsChangedDelegate(PluginListInvalidationKind kind, bool affectedThisPlugin);
/// <summary>
/// Event that gets fired when loc is changed
/// </summary>
public event LanguageChangedDelegate LanguageChanged;
/// <summary>
/// Event that is fired when the active list of plugins is changed.
/// </summary>
public event ActivePluginsChangedDelegate ActivePluginsChanged;
/// <summary>
/// Gets the reason this plugin was loaded.
/// </summary>
public PluginLoadReason Reason { get; }
/// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session.
/// </summary>
public bool IsAutoUpdateComplete => Service<ChatHandlers>.Get().IsAutoUpdateComplete;
/// <summary>
/// Gets the repository from which this plugin was installed.
///
@ -110,12 +125,12 @@ public sealed class DalamudPluginInterface : IDisposable
/// <summary>
/// Gets the current internal plugin name.
/// </summary>
public string InternalName => this.pluginName;
public string InternalName => this.plugin.InternalName;
/// <summary>
/// Gets a value indicating whether this is a dev plugin.
/// </summary>
public bool IsDev { get; }
public bool IsDev => this.plugin.IsDev;
/// <summary>
/// Gets a value indicating whether this is a testing release of a plugin.
@ -148,7 +163,7 @@ public sealed class DalamudPluginInterface : IDisposable
/// <summary>
/// Gets the location of your plugin assembly.
/// </summary>
public FileInfo AssemblyLocation { get; }
public FileInfo AssemblyLocation => this.plugin.DllFile;
/// <summary>
/// Gets the directory your plugin configurations are stored in.
@ -158,7 +173,7 @@ public sealed class DalamudPluginInterface : IDisposable
/// <summary>
/// Gets the config file of your plugin.
/// </summary>
public FileInfo ConfigFile => this.configs.GetConfigFile(this.pluginName);
public FileInfo ConfigFile => this.configs.GetConfigFile(this.plugin.InternalName);
/// <summary>
/// Gets the <see cref="UiBuilder"/> instance which allows you to draw UI into the game via ImGui draw calls.
@ -193,13 +208,20 @@ public sealed class DalamudPluginInterface : IDisposable
/// <summary>
/// Gets a list of installed plugin names.
/// </summary>
[Obsolete($"This property is obsolete. Use {nameof(InstalledPlugins)} instead.")]
public List<string> PluginNames => Service<PluginManager>.Get().InstalledPlugins.Select(p => p.Manifest.Name).ToList();
/// <summary>
/// Gets a list of installed plugin internal names.
/// </summary>
[Obsolete($"This property is obsolete. Use {nameof(InstalledPlugins)} instead.")]
public List<string> PluginInternalNames => Service<PluginManager>.Get().InstalledPlugins.Select(p => p.Manifest.InternalName).ToList();
/// <summary>
/// Gets a list of installed plugins along with their current state.
/// </summary>
public IEnumerable<InstalledPluginState> InstalledPlugins => Service<PluginManager>.Get().InstalledPlugins.Select(p => new InstalledPluginState(p.Name, p.Manifest.InternalName, p.IsLoaded, p.Manifest.EffectiveVersion));
/// <summary>
/// Opens the <see cref="PluginInstallerWindow"/> with the plugin name set as search target.
/// </summary>
@ -213,7 +235,7 @@ public sealed class DalamudPluginInterface : IDisposable
}
dalamudInterface.OpenPluginInstallerPluginInstalled();
dalamudInterface.SetPluginInstallerSearchText(this.pluginName);
dalamudInterface.SetPluginInstallerSearchText(this.plugin.InternalName);
return true;
}
@ -332,7 +354,7 @@ public sealed class DalamudPluginInterface : IDisposable
if (currentConfig == null)
return;
this.configs.Save(currentConfig, this.pluginName);
this.configs.Save(currentConfig, this.plugin.InternalName);
}
/// <summary>
@ -354,30 +376,32 @@ public sealed class DalamudPluginInterface : IDisposable
{
var mi = this.configs.GetType().GetMethod("LoadForType");
var fn = mi.MakeGenericMethod(type);
return (IPluginConfiguration)fn.Invoke(this.configs, new object[] { this.pluginName });
return (IPluginConfiguration)fn.Invoke(this.configs, new object[] { this.plugin.InternalName });
}
}
// this shouldn't be a thing, I think, but just in case
return this.configs.Load(this.pluginName);
return this.configs.Load(this.plugin.InternalName);
}
/// <summary>
/// Get the config directory.
/// </summary>
/// <returns>directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName.</returns>
public string GetPluginConfigDirectory() => this.configs.GetDirectory(this.pluginName);
public string GetPluginConfigDirectory() => this.configs.GetDirectory(this.plugin.InternalName);
/// <summary>
/// Get the loc directory.
/// </summary>
/// <returns>directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName/loc.</returns>
public string GetPluginLocDirectory() => this.configs.GetDirectory(Path.Combine(this.pluginName, "loc"));
public string GetPluginLocDirectory() => this.configs.GetDirectory(Path.Combine(this.plugin.InternalName, "loc"));
#endregion
#region Chat Links
// TODO API9: Move to chatgui, don't allow passing own commandId
/// <summary>
/// Register a chat link handler.
/// </summary>
@ -386,7 +410,7 @@ public sealed class DalamudPluginInterface : IDisposable
/// <returns>Returns an SeString payload for the link.</returns>
public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action<uint, SeString> commandAction)
{
return Service<ChatGui>.Get().AddChatLinkHandler(this.pluginName, commandId, commandAction);
return Service<ChatGui>.Get().AddChatLinkHandler(this.plugin.InternalName, commandId, commandAction);
}
/// <summary>
@ -395,7 +419,7 @@ public sealed class DalamudPluginInterface : IDisposable
/// <param name="commandId">The ID of the command.</param>
public void RemoveChatLinkHandler(uint commandId)
{
Service<ChatGui>.Get().RemoveChatLinkHandler(this.pluginName, commandId);
Service<ChatGui>.Get().RemoveChatLinkHandler(this.plugin.InternalName, commandId);
}
/// <summary>
@ -403,7 +427,7 @@ public sealed class DalamudPluginInterface : IDisposable
/// </summary>
public void RemoveChatLinkHandler()
{
Service<ChatGui>.Get().RemoveChatLinkHandler(this.pluginName);
Service<ChatGui>.Get().RemoveChatLinkHandler(this.plugin.InternalName);
}
#endregion
@ -419,11 +443,9 @@ public sealed class DalamudPluginInterface : IDisposable
{
var svcContainer = Service<IoC.Internal.ServiceContainer>.Get();
var realScopedObjects = new object[scopedObjects.Length + 1];
realScopedObjects[0] = this;
Array.Copy(scopedObjects, 0, realScopedObjects, 1, scopedObjects.Length);
return (T)svcContainer.CreateAsync(typeof(T), realScopedObjects).GetAwaiter().GetResult();
return (T)this.plugin.ServiceScope!.CreateAsync(
typeof(T),
this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult();
}
/// <summary>
@ -434,13 +456,9 @@ public sealed class DalamudPluginInterface : IDisposable
/// <returns>Whether or not the injection succeeded.</returns>
public bool Inject(object instance, params object[] scopedObjects)
{
var svcContainer = Service<IoC.Internal.ServiceContainer>.Get();
var realScopedObjects = new object[scopedObjects.Length + 1];
realScopedObjects[0] = this;
Array.Copy(scopedObjects, 0, realScopedObjects, 1, scopedObjects.Length);
return svcContainer.InjectProperties(instance, realScopedObjects).GetAwaiter().GetResult();
return this.plugin.ServiceScope!.InjectPropertiesAsync(
instance,
this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult();
}
#endregion
@ -451,7 +469,7 @@ public sealed class DalamudPluginInterface : IDisposable
void IDisposable.Dispose()
{
this.UiBuilder.ExplicitDispose();
Service<ChatGui>.Get().RemoveChatLinkHandler(this.pluginName);
Service<ChatGui>.Get().RemoveChatLinkHandler(this.plugin.InternalName);
Service<Localization>.Get().LocalizationChanged -= this.OnLocalizationChanged;
Service<DalamudConfiguration>.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
}
@ -465,6 +483,16 @@ public sealed class DalamudPluginInterface : IDisposable
// ignored
}
/// <summary>
/// Dispatch the active plugins changed event.
/// </summary>
/// <param name="kind">What action caused this event to be fired.</param>
/// <param name="affectedThisPlugin">If this plugin was affected by the change.</param>
internal void NotifyActivePluginsChanged(PluginListInvalidationKind kind, bool affectedThisPlugin)
{
this.ActivePluginsChanged?.Invoke(kind, affectedThisPlugin);
}
private void OnLocalizationChanged(string langCode)
{
this.UiLanguage = langCode;
@ -475,4 +503,9 @@ public sealed class DalamudPluginInterface : IDisposable
{
this.GeneralChatType = dalamudConfiguration.GeneralChatType;
}
private object[] GetPublicIocScopes(IEnumerable<object> scopedObjects)
{
return scopedObjects.Append(this).ToArray();
}
}

View file

@ -0,0 +1,5 @@
using System;
namespace Dalamud.Plugin;
public record InstalledPluginState(string Name, string InternalName, bool IsLoaded, Version Version);

View file

@ -22,6 +22,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.Internal;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Networking.Http;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types;
@ -41,6 +42,9 @@ namespace Dalamud.Plugin.Internal;
// DalamudTextureWrap registers textures to dispose with IM
[InherentDependency<InterfaceManager>]
// LocalPlugin uses ServiceContainer to create scopes
[InherentDependency<ServiceContainer>]
#pragma warning restore SA1015
internal partial class PluginManager : IDisposable, IServiceType
{
@ -54,15 +58,6 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary>
public const int PluginWaitBeforeFreeDefault = 1000; // upped from 500ms, seems more stable
private const string DevPluginsDisclaimerFilename = "DONT_USE_THIS_FOLDER.txt";
private const string DevPluginsDisclaimerText = @"Hey!
The devPlugins folder is deprecated and will be removed soon. Please don't use it anymore for plugin development.
Instead, open the Dalamud settings and add the path to your plugins build output folder as a dev plugin location.
Remove your devPlugin from this folder.
Thanks and have fun!";
private static readonly ModuleLog Log = new("PLUGINM");
private readonly object pluginListLock = new();
@ -81,22 +76,17 @@ Thanks and have fun!";
[ServiceManager.ServiceDependency]
private readonly ProfileManager profileManager = Service<ProfileManager>.Get();
[ServiceManager.ServiceDependency]
private readonly HappyHttpClient happyHttpClient = Service<HappyHttpClient>.Get();
[ServiceManager.ServiceConstructor]
private PluginManager()
{
this.pluginDirectory = new DirectoryInfo(this.startInfo.PluginDirectory!);
this.devPluginDirectory = new DirectoryInfo(this.startInfo.DefaultPluginDirectory!);
if (!this.pluginDirectory.Exists)
this.pluginDirectory.Create();
if (!this.devPluginDirectory.Exists)
this.devPluginDirectory.Create();
var disclaimerFileName = Path.Combine(this.devPluginDirectory.FullName, DevPluginsDisclaimerFilename);
if (!File.Exists(disclaimerFileName))
File.WriteAllText(disclaimerFileName, DevPluginsDisclaimerText);
this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || this.configuration.PluginSafeMode || this.startInfo.NoLoadPlugins;
try
@ -391,9 +381,6 @@ Thanks and have fun!";
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())
{
@ -434,7 +421,7 @@ Thanks and have fun!";
}
// devPlugins are more freeform. Look for any dll and hope to get lucky.
var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
var devDllFiles = new List<FileInfo>();
foreach (var setting in this.configuration.DevPluginLoadLocations)
{
@ -657,11 +644,8 @@ Thanks and have fun!";
/// </summary>
public void ScanDevPlugins()
{
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();
var devDllFiles = new List<FileInfo>();
foreach (var setting in this.configuration.DevPluginLoadLocations)
{
@ -736,7 +720,7 @@ Thanks and have fun!";
var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion;
var response = await Util.HttpClient.GetAsync(downloadUrl);
var response = await this.happyHttpClient.SharedHttpClient.GetAsync(downloadUrl);
response.EnsureSuccessStatusCode();
var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty));
@ -1078,8 +1062,9 @@ Thanks and have fun!";
/// </summary>
/// <param name="ignoreDisabled">Ignore disabled plugins.</param>
/// <param name="dryRun">Perform a dry run, don't install anything.</param>
/// <param name="autoUpdate">If this action was performed as part of an auto-update.</param>
/// <returns>Success or failure and a list of updated plugin metadata.</returns>
public async Task<List<PluginUpdateStatus>> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun)
public async Task<List<PluginUpdateStatus>> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun, bool autoUpdate = false)
{
Log.Information("Starting plugin update");
@ -1104,6 +1089,9 @@ Thanks and have fun!";
}
this.NotifyInstalledPluginsChanged();
this.NotifyPluginsForStateChange(
autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update,
updatedList.Select(x => x.InternalName));
Log.Information("Plugin update OK.");
@ -1394,6 +1382,20 @@ Thanks and have fun!";
this.OnInstalledPluginsChanged?.InvokeSafely();
}
private void NotifyPluginsForStateChange(PluginListInvalidationKind kind, IEnumerable<string> affectedInternalNames)
{
foreach (var installedPlugin in this.InstalledPlugins)
{
if (!installedPlugin.IsLoaded || installedPlugin.DalamudInterface == null)
continue;
installedPlugin.DalamudInterface.NotifyActivePluginsChanged(
kind,
// ReSharper disable once PossibleMultipleEnumeration
affectedInternalNames.Contains(installedPlugin.Manifest.InternalName));
}
}
private static class Locs
{
public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version);

View file

@ -11,6 +11,7 @@ using Dalamud.Game.Gui.Dtr;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.IoC.Internal;
using Dalamud.Logging;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Loader;
@ -181,10 +182,15 @@ internal class LocalPlugin : IDisposable
public AssemblyName? AssemblyName { get; private set; }
/// <summary>
/// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
/// Gets the plugin name from the manifest.
/// </summary>
public string Name => this.Manifest.Name;
/// <summary>
/// Gets the plugin internal name from the manifest.
/// </summary>
public string InternalName => this.Manifest.InternalName;
/// <summary>
/// Gets an optional reason, if the plugin is banned.
/// </summary>
@ -247,6 +253,11 @@ internal class LocalPlugin : IDisposable
public bool ApplicableForLoad => !this.IsBanned && !this.IsDecommissioned && !this.IsOrphaned && !this.IsOutdated
&& !(!this.IsDev && this.State == PluginState.UnloadError) && this.CheckPolicy();
/// <summary>
/// Gets the service scope for this plugin.
/// </summary>
public IServiceScope? ServiceScope { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
@ -268,6 +279,9 @@ internal class LocalPlugin : IDisposable
this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface = null;
this.ServiceScope?.Dispose();
this.ServiceScope = null;
this.pluginType = null;
this.pluginAssembly = null;
@ -314,8 +328,13 @@ internal class LocalPlugin : IDisposable
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");
if (!this.IsDev)
{
throw new InvalidPluginOperationException(
$"Unable to load {this.Name}, load previously faulted, unload first");
}
break;
case PluginState.UnloadError:
if (!this.IsDev)
{
@ -423,17 +442,20 @@ internal class LocalPlugin : IDisposable
PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
this.DalamudInterface =
new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev, this.Manifest);
new DalamudPluginInterface(this, reason);
this.ServiceScope = ioc.GetScope();
this.ServiceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it
if (this.Manifest.LoadSync && this.Manifest.LoadRequiredState is 0 or 1)
{
this.instance = await framework.RunOnFrameworkThread(
() => ioc.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin;
() => this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin;
}
else
{
this.instance =
await ioc.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin;
await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin;
}
if (this.instance == null)
@ -458,7 +480,9 @@ internal class LocalPlugin : IDisposable
catch (Exception ex)
{
this.State = PluginState.LoadError;
Log.Error(ex, $"Error while loading {this.Name}");
if (ex is not BannedPluginException)
Log.Error(ex, $"Error while loading {this.Name}");
throw;
}
@ -479,6 +503,7 @@ internal class LocalPlugin : IDisposable
{
var configuration = Service<DalamudConfiguration>.Get();
var framework = Service<Framework>.GetNullable();
var ioc = await Service<ServiceContainer>.GetAsync();
await this.pluginLoadStateLock.WaitAsync();
try
@ -517,6 +542,9 @@ internal class LocalPlugin : IDisposable
this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface = null;
this.ServiceScope?.Dispose();
this.ServiceScope = null;
this.pluginType = null;
this.pluginAssembly = null;

View file

@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Networking.Http;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal.Types;
@ -24,7 +26,11 @@ internal class PluginRepository
private static readonly ModuleLog Log = new("PLUGINR");
private static readonly HttpClient HttpClient = new()
private static readonly HttpClient HttpClient = new(new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.All,
ConnectCallback = Service<HappyHttpClient>.Get().SharedHappyEyeballsCallback.ConnectCallback,
})
{
Timeout = TimeSpan.FromSeconds(20),
DefaultRequestHeaders =

View file

@ -0,0 +1,17 @@
namespace Dalamud.Plugin;
/// <summary>
/// Causes for a change to the plugin list.
/// </summary>
public enum PluginListInvalidationKind
{
/// <summary>
/// An installer-initiated update reloaded plugins.
/// </summary>
Update,
/// <summary>
/// An auto-update reloaded plugins.
/// </summary>
AutoUpdate,
}