diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 17c091259..c911e73d5 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,7 +27,7 @@ - + diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1007cf9de..d8c6f100a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -65,7 +65,7 @@ - + diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index 52b7031fe..c616e47fd 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using JetBrains.Annotations; namespace Dalamud.Game { @@ -21,7 +22,16 @@ namespace Dalamud.Game protected bool IsResolved { get; set; } /// - /// Setup the resolver, calling the appopriate method based on the process architecture. + /// Setup the resolver, calling the appropriate method based on the process architecture, + /// using the default SigScanner. + /// + /// For plugins. Not intended to be called from Dalamud Service{T} constructors. + /// + [UsedImplicitly] + public void Setup() => this.Setup(Service.Get()); + + /// + /// Setup the resolver, calling the appropriate method based on the process architecture. /// /// The SigScanner instance. public void Setup(SigScanner scanner) diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index 130e9e488..46418384e 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -27,7 +28,7 @@ namespace Dalamud.Game private IntPtr moduleCopyPtr; private long moduleCopyOffset; - private Dictionary? textCache; + private ConcurrentDictionary? textCache; [ServiceManager.ServiceConstructor] private SigScanner(DalamudStartInfo startInfo) @@ -340,9 +341,12 @@ namespace Dalamud.Game /// The real offset of the found signature. public IntPtr ScanText(string signature) { - if (this.textCache != null && this.textCache.TryGetValue(signature, out var address)) + if (this.textCache != null) { - return new IntPtr(address + this.Module.BaseAddress.ToInt64()); + if (this.textCache.TryGetValue(signature, out var address)) + { + return new IntPtr(address + this.Module.BaseAddress.ToInt64()); + } } var mBase = this.IsCopy ? this.moduleCopyPtr : this.TextSectionBase; @@ -356,7 +360,10 @@ namespace Dalamud.Game if (insnByte == 0xE8 || insnByte == 0xE9) scanRet = ReadJmpCallSig(scanRet); - this.textCache?.Add(signature, scanRet.ToInt64() - this.Module.BaseAddress.ToInt64()); + if (this.textCache != null) + { + this.textCache[signature] = scanRet.ToInt64() - this.Module.BaseAddress.ToInt64(); + } return scanRet; } @@ -551,7 +558,7 @@ namespace Dalamud.Game return; } - this.textCache = JsonConvert.DeserializeObject>(File.ReadAllText(this.cacheFile.FullName)) ?? new Dictionary(); + this.textCache = JsonConvert.DeserializeObject>(File.ReadAllText(this.cacheFile.FullName)) ?? new ConcurrentDictionary(); } } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 2eab121c9..da7575990 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -120,11 +120,6 @@ namespace Dalamud.Interface.Internal private delegate void InstallRTSSHook(); - /// - /// Gets a task that gets completed when scene gets initialized. - /// - public Task SceneInitializeTask => this.sceneInitializeTaskCompletionSource.Task; - /// /// This event gets called each frame to facilitate ImGui drawing. /// @@ -165,6 +160,11 @@ namespace Dalamud.Interface.Internal /// public static ImFontPtr MonoFont { get; private set; } + /// + /// Gets a task that gets completed when scene gets initialized. + /// + public Task SceneInitializeTask => this.sceneInitializeTaskCompletionSource.Task; + /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. /// @@ -448,7 +448,6 @@ namespace Dalamud.Interface.Internal try { this.scene = new RawDX11Scene(swapChain); - this.sceneInitializeTaskCompletionSource.SetResult(); } catch (DllNotFoundException ex) { @@ -568,11 +567,17 @@ namespace Dalamud.Interface.Internal this.RenderImGui(); + if (!this.SceneInitializeTask.IsCompleted) + this.sceneInitializeTaskCompletionSource.SetResult(); + return pRes; } this.RenderImGui(); + if (!this.SceneInitializeTask.IsCompleted) + this.sceneInitializeTaskCompletionSource.SetResult(); + return this.presentHook.Original(swapChain, syncInterval, presentFlags); } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 1845156b4..aaedb9d28 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -103,19 +103,22 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller MaximumSize = new Vector2(5000, 5000), }; - var pluginManager = Service.Get(); - - // For debugging - if (pluginManager.PluginsReady) - this.OnInstalledPluginsChanged(); - - pluginManager.OnAvailablePluginsChanged += this.OnAvailablePluginsChanged; - pluginManager.OnInstalledPluginsChanged += this.OnInstalledPluginsChanged; - - for (var i = 0; i < this.testerImagePaths.Length; i++) + Service.GetAsync().ContinueWith(pluginManagerTask => { - this.testerImagePaths[i] = string.Empty; - } + var pluginManager = pluginManagerTask.Result; + + // For debugging + if (pluginManager.PluginsReady) + this.OnInstalledPluginsChanged(); + + pluginManager.OnAvailablePluginsChanged += this.OnAvailablePluginsChanged; + pluginManager.OnInstalledPluginsChanged += this.OnInstalledPluginsChanged; + + for (var i = 0; i < this.testerImagePaths.Length; i++) + { + this.testerImagePaths[i] = string.Empty; + } + }); } private enum OperationStatus diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 518c691bd..bf0e52174 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -16,10 +17,13 @@ using Dalamud.Game; using Dalamud.Game.Gui; using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text; +using Dalamud.Interface; +using Dalamud.Interface.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; +using Dalamud.Utility.Timing; using Newtonsoft.Json; namespace Dalamud.Plugin.Internal; @@ -37,6 +41,7 @@ internal partial class PluginManager : IDisposable private static readonly ModuleLog Log = new("PLUGINM"); + private readonly object pluginListLock = new(); private readonly DirectoryInfo pluginDirectory; private readonly DirectoryInfo devPluginDirectory; private readonly BannedPlugin[] bannedPlugins; @@ -310,13 +315,18 @@ internal partial class PluginManager : IDisposable // Dev plugins should load first. pluginDefs.InsertRange(0, devPluginDefs); - void LoadPlugins(IEnumerable pluginDefsList) + void LoadPluginOnBoot(string logPrefix, PluginDef pluginDef) { - foreach (var pluginDef in pluginDefsList) + using (Timings.Start($"{pluginDef.DllFile.Name}: {logPrefix}Boot")) { try { - this.LoadPlugin(pluginDef.DllFile, pluginDef.Manifest, PluginLoadReason.Boot, pluginDef.IsDev, isBoot: true); + this.LoadPlugin( + pluginDef.DllFile, + pluginDef.Manifest, + PluginLoadReason.Boot, + pluginDef.IsDev, + isBoot: true); } catch (InvalidPluginException) { @@ -324,25 +334,93 @@ internal partial class PluginManager : IDisposable } catch (Exception ex) { - Log.Error(ex, "During boot plugin load, an unexpected error occurred"); + Log.Error(ex, "{0}: During boot plugin load, an unexpected error occurred", logPrefix); } } } - // Load sync plugins - var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadPriority > 0); - LoadPlugins(syncPlugins); + void LoadPluginsSync(string logPrefix, IEnumerable pluginDefsList) + { + foreach (var pluginDef in pluginDefsList) + LoadPluginOnBoot(logPrefix, pluginDef); + } - var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0); - Task.Run(() => LoadPlugins(asyncPlugins)) - .ContinueWith(_ => - { - this.PluginsReady = true; - this.NotifyInstalledPluginsChanged(); + Task LoadPluginsAsync(string logPrefix, IEnumerable pluginDefsList) + { + return Task.WhenAll( + pluginDefsList + .Select(pluginDef => Task.Run(() => LoadPluginOnBoot(logPrefix, pluginDef))) + .ToArray()); + } - // Save signatures, makes sense to do it here since all plugins will be loaded - Service.Get().Save(); - }); + var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadSync == true).ToList(); + var asyncPlugins = pluginDefs.Where(def => def.Manifest?.LoadSync != true).ToList(); + var loadTasks = new List(); + + // Load plugins that can be loaded anytime + LoadPluginsSync( + "AnytimeSync", + syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 2)); + loadTasks.Add( + LoadPluginsAsync( + "AnytimeAsync", + asyncPlugins.Where(def => def.Manifest?.LoadRequiredState == 2))); + + // Load plugins that want to be loaded during Framework.Tick + loadTasks.Add( + Service + .GetAsync() + .ContinueWith( + x => x.Result.RunOnTick( + () => LoadPluginsSync( + "FrameworkTickSync", + syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1))), + TaskContinuationOptions.RunContinuationsAsynchronously) + .Unwrap() + .ContinueWith( + _ => LoadPluginsAsync( + "FrameworkTickAsync", + asyncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1)), + TaskContinuationOptions.RunContinuationsAsynchronously) + .Unwrap()); + + // Load plugins that want to be loaded during Framework.Tick, when drawing facilities are available + loadTasks.Add( + Service + .GetAsync() + .ContinueWith( + x => x.Result.SceneInitializeTask, + TaskContinuationOptions.RunContinuationsAsynchronously) + .Unwrap() + .ContinueWith( + _ => Service.Get().RunOnTick(() => + { + LoadPluginsSync( + "DrawAvailableSync", + syncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null)); + return LoadPluginsAsync( + "DrawAvailableAsync", + asyncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null)); + })) + .Unwrap()); + + // Save signatures when all plugins are done loading, successful or not. + _ = Task + .WhenAll(loadTasks) + .ContinueWith( + _ => Service.GetAsync(), + TaskContinuationOptions.RunContinuationsAsynchronously) + .Unwrap() + .ContinueWith( + sigScannerTask => + { + this.PluginsReady = true; + this.NotifyInstalledPluginsChanged(); + + sigScannerTask.Result.Save(); + }, + TaskContinuationOptions.RunContinuationsAsynchronously) + .ConfigureAwait(false); } /// @@ -352,19 +430,22 @@ internal partial class PluginManager : IDisposable { var aggregate = new List(); - foreach (var plugin in this.InstalledPlugins) + lock (this.pluginListLock) { - if (plugin.IsLoaded) + foreach (var plugin in this.InstalledPlugins) { - try + if (plugin.IsLoaded) { - plugin.Reload(); - } - catch (Exception ex) - { - Log.Error(ex, "Error during reload all"); + try + { + plugin.Reload(); + } + catch (Exception ex) + { + Log.Error(ex, "Error during reload all"); - aggregate.Add(ex); + aggregate.Add(ex); + } } } } @@ -444,8 +525,11 @@ internal partial class PluginManager : IDisposable foreach (var dllFile in devDllFiles) { // This file is already known to us - if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName)) - continue; + lock (this.pluginListLock) + { + if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName)) + continue; + } // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); @@ -624,7 +708,7 @@ internal partial class PluginManager : IDisposable } catch (InvalidPluginException) { - PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); + PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _); throw; } catch (BannedPluginException) @@ -647,13 +731,17 @@ internal partial class PluginManager : IDisposable } else { - PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); + PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _); throw; } } } - this.InstalledPlugins = this.InstalledPlugins.Add(plugin); + lock (this.pluginListLock) + { + this.InstalledPlugins = this.InstalledPlugins.Add(plugin); + } + return plugin; } @@ -666,8 +754,12 @@ internal partial class PluginManager : IDisposable if (plugin.State != PluginState.Unloaded) throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded"); - this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); - PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty); + lock (this.pluginListLock) + { + this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + } + + PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _); this.NotifyInstalledPluginsChanged(); this.NotifyAvailablePluginsChanged(); @@ -834,7 +926,10 @@ internal partial class PluginManager : IDisposable try { plugin.DllFile.Delete(); - this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + lock (this.pluginListLock) + { + this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + } } catch (Exception ex) { @@ -848,7 +943,10 @@ internal partial class PluginManager : IDisposable try { plugin.Disable(); - this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + lock (this.pluginListLock) + { + this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + } } catch (Exception ex) { @@ -1030,7 +1128,7 @@ internal partial class PluginManager /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading /// plugins via byte[]. /// - internal static readonly Dictionary PluginLocations = new(); + internal static readonly ConcurrentDictionary PluginLocations = new(); private MonoMod.RuntimeDetour.Hook? assemblyLocationMonoHook; private MonoMod.RuntimeDetour.Hook? assemblyCodeBaseMonoHook; diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index 422c1b0c8..eaff9e77f 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -134,6 +134,22 @@ internal record PluginManifest [JsonProperty] public string DownloadLinkTesting { get; init; } = null!; + /// + /// Gets the required Dalamud load step for this plugin to load. Takes precedence over LoadPriority. + /// Valid values are: + /// 0. During Framework.Tick, when drawing facilities are available + /// 1. During Framework.Tick + /// 2. No requirement + /// + [JsonProperty] + public int LoadRequiredState { get; init; } + + /// + /// Gets a value indicating whether Dalamud must load this plugin not at the same time with other plugins and the game. + /// + [JsonProperty] + public bool LoadSync { get; init; } + /// /// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority. ///