From 65798841322cff8321ba519a4add89ab4a213a92 Mon Sep 17 00:00:00 2001 From: msdx321 Date: Mon, 29 Dec 2025 16:22:17 +0100 Subject: [PATCH] Add configurable plugin load order with sync/async overrides - add Dev Tools UI to manage a manual load order list - show default load flavor and allow per-plugin override - persist load order and overrides in config - apply ordered loading while respecting sync/async flavor Signed-off-by: msdx321 --- .../Internal/DalamudConfiguration.cs | 10 + .../Internal/PluginLoadOrderMode.cs | 22 ++ .../Internal/PluginCategoryManager.cs | 10 +- .../PluginInstaller/PluginInstallerWindow.cs | 357 ++++++++++++++++++ Dalamud/Plugin/Internal/PluginManager.cs | 86 ++++- 5 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 Dalamud/Configuration/Internal/PluginLoadOrderMode.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index ddcb26914..178beaffe 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -337,6 +337,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public List? DtrOrder { get; set; } + /// + /// Gets or sets the plugin load order, by internal name. + /// + public List? PluginLoadOrder { get; set; } + + /// + /// Gets or sets the plugin load order override mode, by internal name. + /// + public Dictionary? PluginLoadOrderOverrides { get; set; } + /// /// Gets or sets the list of ignored DTR elements, by title. /// diff --git a/Dalamud/Configuration/Internal/PluginLoadOrderMode.cs b/Dalamud/Configuration/Internal/PluginLoadOrderMode.cs new file mode 100644 index 000000000..f92c6320c --- /dev/null +++ b/Dalamud/Configuration/Internal/PluginLoadOrderMode.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Configuration.Internal; + +/// +/// Override for plugin load flavor in the load order list. +/// +internal enum PluginLoadOrderMode +{ + /// + /// Use the plugin's default load flavor. + /// + Default = 0, + + /// + /// Force synchronous load. + /// + ForceSync = 1, + + /// + /// Force asynchronous load. + /// + ForceAsync = 2, +} diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index d3aea7f57..352c50981 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -25,6 +25,7 @@ internal class PluginCategoryManager new(CategoryKind.AvailableForTesting, "special.availableForTesting", () => Locs.Category_AvailableForTesting, CategoryInfo.AppearCondition.DoPluginTest), new(CategoryKind.Hidden, "special.hidden", () => Locs.Category_Hidden, CategoryInfo.AppearCondition.AnyHiddenPlugins), new(CategoryKind.DevInstalled, "special.devInstalled", () => Locs.Category_DevInstalled), + new(CategoryKind.PluginLoadOrder, "special.pluginLoadOrder", () => Locs.Category_PluginLoadOrder), new(CategoryKind.IconTester, "special.devIconTester", () => Locs.Category_IconTester), new(CategoryKind.DalamudChangelogs, "special.dalamud", () => Locs.Category_Dalamud), new(CategoryKind.PluginChangelogs, "special.plugins", () => Locs.Category_Plugins), @@ -46,7 +47,7 @@ internal class PluginCategoryManager private GroupInfo[] groupList = [ - new(GroupKind.DevTools, () => Locs.Group_DevTools, CategoryKind.DevInstalled, CategoryKind.IconTester), + new(GroupKind.DevTools, () => Locs.Group_DevTools, CategoryKind.DevInstalled, CategoryKind.PluginLoadOrder, CategoryKind.IconTester), new(GroupKind.Installed, () => Locs.Group_Installed, CategoryKind.All, CategoryKind.IsTesting, CategoryKind.UpdateablePlugins, CategoryKind.PluginProfiles), new(GroupKind.Available, () => Locs.Group_Available, CategoryKind.All), new(GroupKind.Changelog, () => Locs.Group_Changelog, CategoryKind.All, CategoryKind.DalamudChangelogs, CategoryKind.PluginChangelogs) @@ -141,6 +142,11 @@ internal class PluginCategoryManager /// Updateable plugins. /// UpdateablePlugins = 15, + + /// + /// Customize plugin load order. + /// + PluginLoadOrder = 16, /// /// Plugins tagged as "other". @@ -550,6 +556,8 @@ internal class PluginCategoryManager public static string Category_DevInstalled => Loc.Localize("InstallerInstalledDevPlugins", "Installed Dev Plugins"); + public static string Category_PluginLoadOrder => Loc.Localize("InstallerCategoryPluginLoadOrder", "Plugin Load Order"); + public static string Category_IconTester => "Image/Icon Tester"; public static string Category_PluginProfiles => Loc.Localize("InstallerCategoryPluginProfiles", "Plugin Collections"); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 3241015fc..a547d2106 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -6,6 +6,7 @@ using System.Drawing; using System.IO; using System.Linq; using System.Numerics; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -41,6 +42,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller; /// internal class PluginInstallerWindow : Window, IDisposable { + private const string PluginLoadOrderPayload = "PLUGIN_LOAD_ORDER"; private static readonly ModuleLog Log = new("PLUGINW"); private readonly Vector4 changelogBgColor = new(0.114f, 0.584f, 0.192f, 0.678f); @@ -99,6 +101,7 @@ internal class PluginInstallerWindow : Window, IDisposable private bool deletePluginConfigWarningModalExplainTesting = false; private string deletePluginConfigWarningModalPluginName = string.Empty; private TaskCompletionSource? deletePluginConfigWarningModalTaskCompletionSource; + private string loadOrderAddSelection = string.Empty; private bool feedbackModalDrawing = true; private bool feedbackModalOnNextFrame = false; @@ -1505,6 +1508,320 @@ internal class PluginInstallerWindow : Window, IDisposable } } + private void DrawPluginLoadOrder() + { + var configuration = Service.Get(); + var profileManager = Service.Get(); + var enabledPlugins = this.GetEnabledPlugins(profileManager); + + if (enabledPlugins.Count == 0) + { + DrawMutedBodyText(Locs.LoadOrder_NoEnabledPlugins, 60, 20); + return; + } + + var orderNames = this.GetConfiguredPluginLoadOrder(configuration, enabledPlugins); + var orderedSet = new HashSet(orderNames, StringComparer.OrdinalIgnoreCase); + var pluginLookup = enabledPlugins.ToDictionary(p => p.Manifest.InternalName, StringComparer.OrdinalIgnoreCase); + var availablePlugins = enabledPlugins + .Where(p => !orderedSet.Contains(p.Manifest.InternalName)) + .OrderBy(p => p.Manifest.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + this.CleanupLoadOrderOverrides(configuration, orderedSet); + + ImGui.TextWrapped(Locs.LoadOrder_Hint); + ImGuiHelpers.ScaledDummy(10); + + if (availablePlugins.Count > 0) + { + ImGui.Text(Locs.LoadOrder_AddLabel); + + var selected = availablePlugins.FirstOrDefault(p => p.Manifest.InternalName.Equals(this.loadOrderAddSelection, StringComparison.OrdinalIgnoreCase)); + var preview = selected != null ? selected.Manifest.Name : Locs.LoadOrder_AddPlaceholder; + ImGui.SetNextItemWidth(-1); + if (ImGui.BeginCombo("##PluginLoadOrderAdd"u8, preview)) + { + foreach (var plugin in availablePlugins) + { + var isSelected = plugin.Manifest.InternalName.Equals(this.loadOrderAddSelection, StringComparison.OrdinalIgnoreCase); + if (ImGui.Selectable(plugin.Manifest.Name, isSelected)) + this.loadOrderAddSelection = plugin.Manifest.InternalName; + } + + ImGui.EndCombo(); + } + + if (ImGui.Button(Locs.LoadOrder_AddButton) && selected != null) + { + orderNames.Add(selected.Manifest.InternalName); + this.ApplyPluginLoadOrder(configuration, orderNames); + this.loadOrderAddSelection = string.Empty; + } + + ImGuiHelpers.ScaledDummy(10); + } + + int? moveFrom = null; + int? moveTo = null; + int? removeIndex = null; + Span payloadData = stackalloc byte[sizeof(int)]; + + using var listChild = ImRaii.Child("PluginLoadOrderList"u8, new Vector2(-1, -1), true); + if (!listChild) + return; + + if (orderNames.Count == 0) + { + DrawMutedBodyText(Locs.LoadOrder_NoneSelected, 20, 10); + return; + } + + if (!ImGui.BeginTable("PluginLoadOrderTable"u8, 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchProp)) + return; + + ImGui.TableSetupColumn(Locs.LoadOrder_ColumnName, ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn(Locs.LoadOrder_ColumnDefault, ImGuiTableColumnFlags.WidthFixed, 90 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn(Locs.LoadOrder_ColumnOverride, ImGuiTableColumnFlags.WidthFixed, 130 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn(Locs.LoadOrder_ColumnRemove, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize(Locs.LoadOrder_RemoveButton).X + (ImGui.GetStyle().FramePadding.X * 2)); + ImGui.TableHeadersRow(); + + for (var i = 0; i < orderNames.Count; i++) + { + var internalName = orderNames[i]; + if (!pluginLookup.TryGetValue(internalName, out var plugin)) + continue; + + ImGui.PushID(internalName); + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + ImGui.Selectable($"{plugin.Manifest.Name}###PluginLoadOrder_{internalName}", false); + + if (ImGui.BeginDragDropSource()) + { + unsafe + { + var payloadIndex = i; + MemoryMarshal.Write(payloadData, in payloadIndex); + ImGui.SetDragDropPayload(PluginLoadOrderPayload, payloadData); + } + + ImGui.Text(plugin.Manifest.Name); + ImGui.EndDragDropSource(); + } + + if (ImGui.BeginDragDropTarget()) + { + unsafe + { + var payload = ImGui.AcceptDragDropPayload(PluginLoadOrderPayload); + if (!payload.IsNull && payload.Data != null && payload.DataSize >= sizeof(int)) + { + var sourceIndex = MemoryMarshal.Read(new ReadOnlySpan(payload.Data, payload.DataSize)); + moveFrom = sourceIndex; + moveTo = i; + } + } + + ImGui.EndDragDropTarget(); + } + + var defaultIsSync = plugin.Manifest is LocalPluginManifest localManifest && localManifest.LoadSync; + ImGui.TableSetColumnIndex(1); + ImGui.Text(defaultIsSync ? Locs.LoadOrder_DefaultSync : Locs.LoadOrder_DefaultAsync); + + ImGui.TableSetColumnIndex(2); + var overrideMode = this.GetLoadOrderOverride(configuration, internalName); + var overridePreview = overrideMode switch + { + PluginLoadOrderMode.ForceSync => Locs.LoadOrder_OverrideSync, + PluginLoadOrderMode.ForceAsync => Locs.LoadOrder_OverrideAsync, + _ => Locs.LoadOrder_OverrideDefault, + }; + + ImGui.SetNextItemWidth(-1); + if (ImGui.BeginCombo("##PluginLoadOrderOverride"u8, overridePreview)) + { + if (ImGui.Selectable(Locs.LoadOrder_OverrideDefault, overrideMode == PluginLoadOrderMode.Default)) + this.SetLoadOrderOverride(configuration, internalName, PluginLoadOrderMode.Default); + if (ImGui.Selectable(Locs.LoadOrder_OverrideSync, overrideMode == PluginLoadOrderMode.ForceSync)) + this.SetLoadOrderOverride(configuration, internalName, PluginLoadOrderMode.ForceSync); + if (ImGui.Selectable(Locs.LoadOrder_OverrideAsync, overrideMode == PluginLoadOrderMode.ForceAsync)) + this.SetLoadOrderOverride(configuration, internalName, PluginLoadOrderMode.ForceAsync); + ImGui.EndCombo(); + } + + ImGui.TableSetColumnIndex(3); + if (ImGui.SmallButton(Locs.LoadOrder_RemoveButton)) + removeIndex = i; + + ImGui.PopID(); + } + + ImGui.EndTable(); + + if (removeIndex.HasValue) + { + var removedInternalName = orderNames[removeIndex.Value]; + orderNames.RemoveAt(removeIndex.Value); + this.SetLoadOrderOverride(configuration, removedInternalName, PluginLoadOrderMode.Default); + this.ApplyPluginLoadOrder(configuration, orderNames); + } + + if (moveFrom.HasValue && moveTo.HasValue && moveFrom != moveTo) + { + var item = orderNames[moveFrom.Value]; + orderNames.RemoveAt(moveFrom.Value); + if (moveFrom.Value < moveTo.Value) + moveTo--; + orderNames.Insert(moveTo.Value, item); + + this.ApplyPluginLoadOrder(configuration, orderNames); + } + } + + private List GetEnabledPlugins(ProfileManager profileManager) + { + var enabledPlugins = new List(); + using var scope = profileManager.GetSyncScope(); + + foreach (var plugin in this.pluginListInstalled) + { + if (plugin.State == PluginState.Loaded) + { + enabledPlugins.Add(plugin); + continue; + } + + foreach (var profile in profileManager.Profiles) + { + if (!profile.IsEnabled) + continue; + + var wants = profile.WantsPlugin(plugin.EffectiveWorkingPluginId); + if (wants == true) + { + enabledPlugins.Add(plugin); + break; + } + } + } + + return enabledPlugins; + } + + private List GetConfiguredPluginLoadOrder(DalamudConfiguration configuration, IReadOnlyList enabledPlugins) + { + var enabledNames = new HashSet(enabledPlugins.Select(p => p.Manifest.InternalName), StringComparer.OrdinalIgnoreCase); + var normalized = new List(enabledPlugins.Count); + var normalizedSet = new HashSet(StringComparer.OrdinalIgnoreCase); + var changed = false; + + if (configuration.PluginLoadOrder != null) + { + foreach (var internalName in configuration.PluginLoadOrder) + { + if (internalName.IsNullOrEmpty()) + { + changed = true; + continue; + } + + if (!enabledNames.Contains(internalName)) + { + changed = true; + continue; + } + + if (normalizedSet.Add(internalName)) + { + normalized.Add(internalName); + } + else + { + changed = true; + } + } + } + + if (changed) + { + this.ApplyPluginLoadOrder(configuration, normalized); + } + + return normalized; + } + + private void ApplyPluginLoadOrder(DalamudConfiguration configuration, IReadOnlyList orderNames) + { + if (configuration.PluginLoadOrder != null && + configuration.PluginLoadOrder.SequenceEqual(orderNames, StringComparer.OrdinalIgnoreCase)) + { + return; + } + + configuration.PluginLoadOrder = orderNames.ToList(); + configuration.QueueSave(); + } + + private void CleanupLoadOrderOverrides(DalamudConfiguration configuration, HashSet orderedSet) + { + if (configuration.PluginLoadOrderOverrides == null || configuration.PluginLoadOrderOverrides.Count == 0) + return; + + var removedAny = false; + var keysToRemove = configuration.PluginLoadOrderOverrides.Keys + .Where(key => !orderedSet.Contains(key)) + .ToList(); + foreach (var key in keysToRemove) + { + configuration.PluginLoadOrderOverrides.Remove(key); + removedAny = true; + } + + if (configuration.PluginLoadOrderOverrides.Count == 0) + configuration.PluginLoadOrderOverrides = null; + + if (removedAny) + configuration.QueueSave(); + } + + private PluginLoadOrderMode GetLoadOrderOverride(DalamudConfiguration configuration, string internalName) + { + if (configuration.PluginLoadOrderOverrides == null) + return PluginLoadOrderMode.Default; + + return configuration.PluginLoadOrderOverrides.TryGetValue(internalName, out var mode) + ? mode + : PluginLoadOrderMode.Default; + } + + private void SetLoadOrderOverride(DalamudConfiguration configuration, string internalName, PluginLoadOrderMode mode) + { + if (mode == PluginLoadOrderMode.Default) + { + if (configuration.PluginLoadOrderOverrides == null) + return; + + if (configuration.PluginLoadOrderOverrides.Remove(internalName)) + { + if (configuration.PluginLoadOrderOverrides.Count == 0) + configuration.PluginLoadOrderOverrides = null; + configuration.QueueSave(); + } + + return; + } + + configuration.PluginLoadOrderOverrides ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (configuration.PluginLoadOrderOverrides.TryGetValue(internalName, out var existing) && existing == mode) + return; + + configuration.PluginLoadOrderOverrides[internalName] = mode; + configuration.QueueSave(); + } + private void DrawPluginCategories() { var useContentHeight = -40f; // button height + spacing @@ -1700,6 +2017,10 @@ internal class PluginInstallerWindow : Window, IDisposable this.DrawInstalledPluginList(InstalledPluginListFilter.Dev); break; + case PluginCategoryManager.CategoryKind.PluginLoadOrder: + this.DrawPluginLoadOrder(); + break; + case PluginCategoryManager.CategoryKind.IconTester: this.DrawImageTester(); break; @@ -4099,6 +4420,42 @@ internal class PluginInstallerWindow : Window, IDisposable #endregion + #region Load order + + public static string LoadOrder_Hint => Loc.Localize("InstallerPluginLoadOrderHint", "Plugins added here load in the order shown. Default load flavor is shown and can be overridden. Changes apply the next time Dalamud starts."); + + public static string LoadOrder_NoEnabledPlugins => Loc.Localize("InstallerPluginLoadOrderNone", "No enabled plugins were found."); + + public static string LoadOrder_NoneSelected => Loc.Localize("InstallerPluginLoadOrderEmpty", "No plugins are selected yet."); + + public static string LoadOrder_AddLabel => Loc.Localize("InstallerPluginLoadOrderAddLabel", "Add an enabled plugin:"); + + public static string LoadOrder_AddPlaceholder => Loc.Localize("InstallerPluginLoadOrderAddPlaceholder", "Select a plugin..."); + + public static string LoadOrder_AddButton => Loc.Localize("InstallerPluginLoadOrderAddButton", "Add to load order"); + + public static string LoadOrder_RemoveButton => Loc.Localize("InstallerPluginLoadOrderRemoveButton", "Remove"); + + public static string LoadOrder_ColumnName => Loc.Localize("InstallerPluginLoadOrderColumnName", "Name"); + + public static string LoadOrder_ColumnDefault => Loc.Localize("InstallerPluginLoadOrderColumnDefault", "Default"); + + public static string LoadOrder_ColumnOverride => Loc.Localize("InstallerPluginLoadOrderColumnOverride", "Override"); + + public static string LoadOrder_ColumnRemove => Loc.Localize("InstallerPluginLoadOrderColumnRemove", "Remove"); + + public static string LoadOrder_DefaultSync => Loc.Localize("InstallerPluginLoadOrderDefaultSync", "Sync"); + + public static string LoadOrder_DefaultAsync => Loc.Localize("InstallerPluginLoadOrderDefaultAsync", "Async"); + + public static string LoadOrder_OverrideDefault => Loc.Localize("InstallerPluginLoadOrderOverrideDefault", "Default"); + + public static string LoadOrder_OverrideSync => Loc.Localize("InstallerPluginLoadOrderOverrideSync", "Force Sync"); + + public static string LoadOrder_OverrideAsync => Loc.Localize("InstallerPluginLoadOrderOverrideAsync", "Force Async"); + + #endregion + #region Search text public static string TabBody_SearchNoMatching => Loc.Localize("InstallerNoMatching", "No plugins were found matching your search."); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 193a2d45f..f030bc715 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -620,6 +620,21 @@ internal class PluginManager : IInternalDisposableService Log.Information($"============= LoadPluginsAsync({logPrefix}) END ============="); } + async Task LoadPluginsAsyncOrdered(string logPrefix, IEnumerable pluginDefsList, CancellationToken token) + { + Log.Information($"============= LoadPluginsAsyncOrdered({logPrefix}) START ============="); + + await Task.Run( + async () => + { + foreach (var pluginDef in pluginDefsList) + await LoadPluginOnBoot(logPrefix, pluginDef, token).ConfigureAwait(false); + }, + token).ConfigureAwait(false); + + Log.Information($"============= LoadPluginsAsyncOrdered({logPrefix}) END ============="); + } + // Initialize the startup load tracker for all LoadSync plugins { this.StartupLoadTracking = new(); @@ -629,8 +644,15 @@ internal class PluginManager : IInternalDisposableService } } - var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadSync == true).ToList(); - var asyncPlugins = pluginDefs.Where(def => def.Manifest?.LoadSync != true).ToList(); + // Respect the original load flavor for ordered plugins. + var (orderedSyncPluginDefs, orderedAsyncPluginDefs) = this.GetOrderedPluginDefsByLoadFlavor(pluginDefs); + var orderedSet = new HashSet( + orderedSyncPluginDefs.Select(def => def.Manifest.InternalName) + .Concat(orderedAsyncPluginDefs.Select(def => def.Manifest.InternalName)), + StringComparer.OrdinalIgnoreCase); + + var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadSync == true && !orderedSet.Contains(def.Manifest.InternalName)).ToList(); + var asyncPlugins = pluginDefs.Where(def => def.Manifest?.LoadSync != true && !orderedSet.Contains(def.Manifest.InternalName)).ToList(); var loadTasks = new List(); var tokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(5)); @@ -638,8 +660,13 @@ internal class PluginManager : IInternalDisposableService // Load plugins that can be loaded anytime await LoadPluginsSync( "AnytimeSync", - syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 2), + orderedSyncPluginDefs.Where(def => def.Manifest?.LoadRequiredState == 2) + .Concat(syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 2)), tokenSource.Token); + loadTasks.Add(LoadPluginsAsyncOrdered( + "AnytimeOrderedAsync", + orderedAsyncPluginDefs.Where(def => def.Manifest?.LoadRequiredState == 2), + tokenSource.Token)); loadTasks.Add(LoadPluginsAsync( "AnytimeAsync", asyncPlugins.Where(def => def.Manifest?.LoadRequiredState == 2), @@ -656,11 +683,16 @@ internal class PluginManager : IInternalDisposableService await framework.RunOnTick( () => LoadPluginsSync( "FrameworkTickSync", - syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1), + orderedSyncPluginDefs.Where(def => def.Manifest?.LoadRequiredState == 1) + .Concat(syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1)), tokenSource.Token), cancellationToken: tokenSource.Token).ConfigureAwait(false); Log.Verbose("Loaded FrameworkTickSync plugins (LoadRequiredState == 1)"); + loadTasks.Add(LoadPluginsAsyncOrdered( + "FrameworkTickOrderedAsync", + orderedAsyncPluginDefs.Where(def => def.Manifest?.LoadRequiredState == 1), + tokenSource.Token)); loadTasks.Add(LoadPluginsAsync( "FrameworkTickAsync", asyncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1), @@ -673,11 +705,16 @@ internal class PluginManager : IInternalDisposableService await framework.RunOnTick( () => LoadPluginsSync( "DrawAvailableSync", - syncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null), + orderedSyncPluginDefs.Where(def => def.Manifest?.LoadRequiredState is 0 or null) + .Concat(syncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null)), tokenSource.Token), cancellationToken: tokenSource.Token); Log.Verbose("Loaded DrawAvailableSync plugins (LoadRequiredState == 0 or null)"); + loadTasks.Add(LoadPluginsAsyncOrdered( + "DrawAvailableOrderedAsync", + orderedAsyncPluginDefs.Where(def => def.Manifest?.LoadRequiredState is 0 or null), + tokenSource.Token)); loadTasks.Add(LoadPluginsAsync( "DrawAvailableAsync", asyncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null), @@ -1362,6 +1399,45 @@ internal class PluginManager : IInternalDisposableService } } + private (List Sync, List Async) GetOrderedPluginDefsByLoadFlavor(List pluginDefs) + { + var configuredOrder = this.configuration.PluginLoadOrder; + if (configuredOrder == null || configuredOrder.Count == 0) + return (new List(), new List()); + + var pluginLookup = pluginDefs.ToDictionary(def => def.Manifest.InternalName, StringComparer.OrdinalIgnoreCase); + var overrides = this.configuration.PluginLoadOrderOverrides; + var orderedSync = new List(configuredOrder.Count); + var orderedAsync = new List(configuredOrder.Count); + + foreach (var internalName in configuredOrder) + { + if (internalName.IsNullOrEmpty()) + continue; + + if (!pluginLookup.TryGetValue(internalName, out var pluginDef)) + continue; + + var mode = PluginLoadOrderMode.Default; + if (overrides != null && overrides.TryGetValue(internalName, out var overrideMode)) + mode = overrideMode; + + var isSync = mode switch + { + PluginLoadOrderMode.ForceSync => true, + PluginLoadOrderMode.ForceAsync => false, + _ => pluginDef.Manifest.LoadSync, + }; + + if (isSync) + orderedSync.Add(pluginDef); + else + orderedAsync.Add(pluginDef); + } + + return (orderedSync, orderedAsync); + } + /// /// Check if there are any inconsistencies with our plugins, their IDs, and our profiles. ///