From 8276c19c6f016d58da414916074c4a5a764ad26e Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 23 Dec 2024 20:55:57 +0100 Subject: [PATCH] add plugin enable/disable/toggle commands --- .../PluginInstaller/ProfileManagerWidget.cs | 6 +- .../PluginManagementCommandHandler.cs | 318 ++++++++++++++---- 2 files changed, 254 insertions(+), 70 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index ee93d2042..ddb89d38c 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -625,13 +625,13 @@ internal class ProfileManagerWidget Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:"); public static string TutorialCommandsEnable => - Loc.Localize("ProfileManagerTutorialCommandsEnable", "{0} \"Collection Name\" - Enable a collection").Format(PluginManagementCommandHandler.CommandEnable); + Loc.Localize("ProfileManagerTutorialCommandsEnable", "{0} \"Collection Name\" - Enable a collection").Format(PluginManagementCommandHandler.CommandEnableProfile); public static string TutorialCommandsDisable => - Loc.Localize("ProfileManagerTutorialCommandsDisable", "{0} \"Collection Name\" - Disable a collection").Format(PluginManagementCommandHandler.CommandDisable); + Loc.Localize("ProfileManagerTutorialCommandsDisable", "{0} \"Collection Name\" - Disable a collection").Format(PluginManagementCommandHandler.CommandDisableProfile); public static string TutorialCommandsToggle => - Loc.Localize("ProfileManagerTutorialCommandsToggle", "{0} \"Collection Name\" - Toggle a collection's state").Format(PluginManagementCommandHandler.CommandToggle); + Loc.Localize("ProfileManagerTutorialCommandsToggle", "{0} \"Collection Name\" - Toggle a collection's state").Format(PluginManagementCommandHandler.CommandToggleProfile); public static string TutorialCommandsEnd => Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order."); diff --git a/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs index d219b659b..ad5aad286 100644 --- a/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs @@ -6,6 +6,7 @@ using CheapLoc; using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Game.Gui; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -19,50 +20,62 @@ namespace Dalamud.Plugin.Internal.Profiles; internal class PluginManagementCommandHandler : IInternalDisposableService { #pragma warning disable SA1600 - public const string CommandEnable = "/xlenablecollection"; - public const string CommandDisable = "/xldisablecollection"; - public const string CommandToggle = "/xltogglecollection"; + public const string CommandEnableProfile = "/xlenablecollection"; + public const string CommandDisableProfile = "/xldisablecollection"; + public const string CommandToggleProfile = "/xltogglecollection"; + + public const string CommandEnablePlugin = "/xlenableplugin"; + public const string CommandDisablePlugin = "/xldisableplugin"; + public const string CommandTogglePlugin = "/xltoggleplugin"; #pragma warning restore SA1600 - private static readonly string LegacyCommandEnable = CommandEnable.Replace("collection", "profile"); - private static readonly string LegacyCommandDisable = CommandDisable.Replace("collection", "profile"); - private static readonly string LegacyCommandToggle = CommandToggle.Replace("collection", "profile"); + private static readonly string LegacyCommandEnable = CommandEnableProfile.Replace("collection", "profile"); + private static readonly string LegacyCommandDisable = CommandDisableProfile.Replace("collection", "profile"); + private static readonly string LegacyCommandToggle = CommandToggleProfile.Replace("collection", "profile"); private readonly CommandManager cmd; private readonly ProfileManager profileManager; + private readonly PluginManager pluginManager; private readonly ChatGui chat; private readonly Framework framework; - private List<(string, ProfileOp)> queue = new(); - + private List<(Target Target, PluginCommandOperation Operation)> commandQueue = new(); + /// /// Initializes a new instance of the class. /// /// Command handler. /// Profile manager. + /// Plugin manager. /// Chat handler. /// Framework. [ServiceManager.ServiceConstructor] - public PluginManagementCommandHandler(CommandManager cmd, ProfileManager profileManager, ChatGui chat, Framework framework) + public PluginManagementCommandHandler( + CommandManager cmd, + ProfileManager profileManager, + PluginManager pluginManager, + ChatGui chat, + Framework framework) { this.cmd = cmd; this.profileManager = profileManager; + this.pluginManager = pluginManager; this.chat = chat; this.framework = framework; - this.cmd.AddHandler(CommandEnable, new CommandInfo(this.OnEnableProfile) + this.cmd.AddHandler(CommandEnableProfile, new CommandInfo(this.OnEnableProfile) { HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""), ShowInHelp = true, }); - this.cmd.AddHandler(CommandDisable, new CommandInfo(this.OnDisableProfile) + this.cmd.AddHandler(CommandDisableProfile, new CommandInfo(this.OnDisableProfile) { HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""), ShowInHelp = true, }); - this.cmd.AddHandler(CommandToggle, new CommandInfo(this.OnToggleProfile) + this.cmd.AddHandler(CommandToggleProfile, new CommandInfo(this.OnToggleProfile) { HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""), ShowInHelp = true, @@ -75,18 +88,36 @@ internal class PluginManagementCommandHandler : IInternalDisposableService this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile) { - ShowInHelp = true, + ShowInHelp = false, }); this.cmd.AddHandler(LegacyCommandToggle, new CommandInfo(this.OnToggleProfile) { + ShowInHelp = false, + }); + + this.cmd.AddHandler(CommandEnablePlugin, new CommandInfo(this.OnEnablePlugin) + { + HelpMessage = Loc.Localize("PluginCommandsEnableHint", "Enable a plugin. Usage: /xlenableplugin \"Plugin Name\""), + ShowInHelp = true, + }); + + this.cmd.AddHandler(CommandDisablePlugin, new CommandInfo(this.OnDisablePlugin) + { + HelpMessage = Loc.Localize("PluginCommandsDisableHint", "Disable a plugin. Usage: /xldisableplugin \"Plugin Name\""), + ShowInHelp = true, + }); + + this.cmd.AddHandler(CommandTogglePlugin, new CommandInfo(this.OnTogglePlugin) + { + HelpMessage = Loc.Localize("PluginCommandsToggleHint", "Toggle a plugin. Usage: /xltoggleplugin \"Plugin Name\""), ShowInHelp = true, }); this.framework.Update += this.FrameworkOnUpdate; } - private enum ProfileOp + private enum PluginCommandOperation { Enable, Disable, @@ -96,109 +127,262 @@ internal class PluginManagementCommandHandler : IInternalDisposableService /// void IInternalDisposableService.DisposeService() { - this.cmd.RemoveHandler(CommandEnable); - this.cmd.RemoveHandler(CommandDisable); - this.cmd.RemoveHandler(CommandToggle); + this.cmd.RemoveHandler(CommandEnableProfile); + this.cmd.RemoveHandler(CommandDisableProfile); + this.cmd.RemoveHandler(CommandToggleProfile); this.cmd.RemoveHandler(LegacyCommandEnable); this.cmd.RemoveHandler(LegacyCommandDisable); this.cmd.RemoveHandler(LegacyCommandToggle); this.framework.Update += this.FrameworkOnUpdate; } - - private void FrameworkOnUpdate(IFramework framework1) + + private void HandleProfileOperation(string profileName, PluginCommandOperation operation) { - if (this.profileManager.IsBusy) + var profile = this.profileManager.Profiles.FirstOrDefault( + x => x.Name == profileName); + if (profile == null || profile.IsDefaultProfile) return; - if (this.queue.Count > 0) + switch (operation) { - var op = this.queue[0]; - this.queue.RemoveAt(0); + case PluginCommandOperation.Enable: + if (!profile.IsEnabled) + Task.Run(() => profile.SetStateAsync(true, false)).GetAwaiter().GetResult(); + break; + case PluginCommandOperation.Disable: + if (profile.IsEnabled) + Task.Run(() => profile.SetStateAsync(false, false)).GetAwaiter().GetResult(); + break; + case PluginCommandOperation.Toggle: + Task.Run(() => profile.SetStateAsync(!profile.IsEnabled, false)).GetAwaiter().GetResult(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(operation), operation, null); + } - var profile = this.profileManager.Profiles.FirstOrDefault(x => x.Name == op.Item1); - if (profile == null || profile.IsDefaultProfile) - return; + this.chat.Print( + profile.IsEnabled + ? Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name) + : Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name)); - switch (op.Item2) + Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t => + { + if (!t.IsCompletedSuccessfully && t.Exception != null) { - case ProfileOp.Enable: - if (!profile.IsEnabled) - Task.Run(() => profile.SetStateAsync(true, false)).GetAwaiter().GetResult(); - break; - case ProfileOp.Disable: - if (profile.IsEnabled) - Task.Run(() => profile.SetStateAsync(false, false)).GetAwaiter().GetResult(); - break; - case ProfileOp.Toggle: - Task.Run(() => profile.SetStateAsync(!profile.IsEnabled, false)).GetAwaiter().GetResult(); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - if (profile.IsEnabled) - { - this.chat.Print(Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name)); + Log.Error(t.Exception, "Could not apply profiles through commands"); + this.chat.PrintError(Loc.Localize("ProfileCommandsApplyFailed", "Failed to apply your collections. Please check the console for errors.")); } else { - this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name)); + this.chat.Print(Loc.Localize("ProfileCommandsApplySuccess", "Collections applied.")); } + }); + } + + private bool HandlePluginOperation(Guid workingPluginId, PluginCommandOperation operation) + { + var plugin = this.pluginManager.InstalledPlugins.FirstOrDefault(x => x.EffectiveWorkingPluginId == workingPluginId); + if (plugin == null) + return true; - Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t => + switch (plugin.State) + { + // Ignore if the plugin is in a fail state + case PluginState.LoadError or PluginState.UnloadError: + this.chat.Print(Loc.Localize("PluginCommandsFailed", "Plugin \"{0}\" has previously failed to load/unload, not continuing.").Format(plugin.Name)); + return true; + + case PluginState.Loaded when operation == PluginCommandOperation.Enable: + this.chat.Print(Loc.Localize("PluginCommandsAlreadyEnabled", "Plugin \"{0}\" is already enabled.").Format(plugin.Name)); + return true; + case PluginState.Unloaded when operation == PluginCommandOperation.Disable: + this.chat.Print(Loc.Localize("PluginCommandsAlreadyDisabled", "Plugin \"{0}\" is already disabled.").Format(plugin.Name)); + return true; + + // Defer if this plugin is busy right now + case PluginState.Loading or PluginState.Unloading: + return false; + } + + void Continuation(Task t, string onSuccess, string onError) + { + if (!t.IsCompletedSuccessfully && t.Exception != null) { - if (!t.IsCompletedSuccessfully && t.Exception != null) - { - Log.Error(t.Exception, "Could not apply profiles through commands"); - this.chat.PrintError(Loc.Localize("ProfileCommandsApplyFailed", "Failed to apply your collections. Please check the console for errors.")); - } - else - { - this.chat.Print(Loc.Localize("ProfileCommandsApplySuccess", "Collections applied.")); - } - }); + Log.Error(t.Exception, "Plugin command operation failed for plugin {PluginName}", plugin.Name); + this.chat.PrintError(onError); + return; + } + + this.chat.Print(onSuccess); + } + + switch (operation) + { + case PluginCommandOperation.Enable: + this.chat.Print(Loc.Localize("PluginCommandsEnabling", "Enabling plugin \"{0}\"...").Format(plugin.Name)); + Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer)) + .ContinueWith(t => Continuation(t, + Loc.Localize("PluginCommandsEnableSuccess", "Plugin \"{0}\" enabled.").Format(plugin.Name), + Loc.Localize("PluginCommandsEnableFailed", "Failed to enable plugin \"{0}\". Please check the console for errors.").Format(plugin.Name))) + .ConfigureAwait(false); + break; + case PluginCommandOperation.Disable: + this.chat.Print(Loc.Localize("PluginCommandsDisabling", "Disabling plugin \"{0}\"...").Format(plugin.Name)); + Task.Run(() => plugin.UnloadAsync()) + .ContinueWith(t => Continuation(t, + Loc.Localize("PluginCommandsDisableSuccess", "Plugin \"{0}\" disabled.").Format(plugin.Name), + Loc.Localize("PluginCommandsDisableFailed", "Failed to disable plugin \"{0}\". Please check the console for errors.").Format(plugin.Name))) + .ConfigureAwait(false); + break; + case PluginCommandOperation.Toggle: + this.chat.Print(Loc.Localize("PluginCommandsToggling", "Toggling plugin \"{0}\"...").Format(plugin.Name)); + Task.Run(() => plugin.State == PluginState.Loaded ? plugin.UnloadAsync() : plugin.LoadAsync(PluginLoadReason.Installer)) + .ContinueWith(t => Continuation(t, + Loc.Localize("PluginCommandsToggleSuccess", "Plugin \"{0}\" toggled.").Format(plugin.Name), + Loc.Localize("PluginCommandsToggleFailed", "Failed to toggle plugin \"{0}\". Please check the console for errors.").Format(plugin.Name))) + .ConfigureAwait(false); + break; + default: + throw new ArgumentOutOfRangeException(nameof(operation), operation, null); + } + + return true; + } + + private void FrameworkOnUpdate(IFramework framework1) + { + if (this.profileManager.IsBusy) + { + return; + } + + if (this.commandQueue.Count > 0) + { + var op = this.commandQueue[0]; + + var remove = true; + switch (op.Target) + { + case PluginTarget pluginTarget: + remove = this.HandlePluginOperation(pluginTarget.WorkingPluginId, op.Operation); + break; + case ProfileTarget profileTarget: + this.HandleProfileOperation(profileTarget.ProfileName, op.Operation); + break; + } + + if (remove) + { + this.commandQueue.RemoveAt(0); + } } } private void OnEnableProfile(string command, string arguments) { - var name = this.ValidateName(arguments); + var name = this.ValidateProfileName(arguments); if (name == null) return; - this.queue = this.queue.Where(x => x.Item1 != name).ToList(); - this.queue.Add((name, ProfileOp.Enable)); + var target = new ProfileTarget(name); + this.commandQueue = this.commandQueue.Where(x => x.Target != target).ToList(); + this.commandQueue.Add((target, PluginCommandOperation.Enable)); } private void OnDisableProfile(string command, string arguments) { - var name = this.ValidateName(arguments); + var name = this.ValidateProfileName(arguments); if (name == null) return; - this.queue = this.queue.Where(x => x.Item1 != name).ToList(); - this.queue.Add((name, ProfileOp.Disable)); + var target = new ProfileTarget(name); + this.commandQueue = this.commandQueue.Where(x => x.Target != target).ToList(); + this.commandQueue.Add((target, PluginCommandOperation.Disable)); } private void OnToggleProfile(string command, string arguments) { - var name = this.ValidateName(arguments); + var name = this.ValidateProfileName(arguments); if (name == null) return; - this.queue.Add((name, ProfileOp.Toggle)); + var target = new ProfileTarget(name); + this.commandQueue.Add((target, PluginCommandOperation.Toggle)); + } + + private void OnEnablePlugin(string command, string arguments) + { + var plugin = this.ValidatePluginName(arguments); + if (plugin == null) + return; + + var target = new PluginTarget(plugin.EffectiveWorkingPluginId); + this.commandQueue + .RemoveAll(x => x.Target == target); + this.commandQueue.Add((target, PluginCommandOperation.Enable)); + } + + private void OnDisablePlugin(string command, string arguments) + { + var plugin = this.ValidatePluginName(arguments); + if (plugin == null) + return; + + var target = new PluginTarget(plugin.EffectiveWorkingPluginId); + this.commandQueue + .RemoveAll(x => x.Target == target); + this.commandQueue.Add((target, PluginCommandOperation.Disable)); + } + + private void OnTogglePlugin(string command, string arguments) + { + var plugin = this.ValidatePluginName(arguments); + if (plugin == null) + return; + + var target = new PluginTarget(plugin.EffectiveWorkingPluginId); + this.commandQueue + .RemoveAll(x => x.Target == target); + this.commandQueue.Add((target, PluginCommandOperation.Toggle)); } - private string? ValidateName(string arguments) + private string? ValidateProfileName(string arguments) { var name = arguments.Replace("\"", string.Empty); if (this.profileManager.Profiles.All(x => x.Name != name)) { - this.chat.PrintError($"No collection like \"{name}\"."); + this.chat.PrintError(Loc.Localize("ProfileCommandsNotFound", "Collection \"{0}\" not found.").Format(name)); return null; } return name; } + + private LocalPlugin? ValidatePluginName(string arguments) + { + var name = arguments.Replace("\"", string.Empty); + var targetPlugin = + this.pluginManager.InstalledPlugins.FirstOrDefault(x => x.InternalName == name || x.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase)); + + if (targetPlugin == null) + { + this.chat.PrintError(Loc.Localize("PluginCommandsNotFound", "Plugin \"{0}\" not found.").Format(name)); + return null; + } + + if (!this.profileManager.IsInDefaultProfile(targetPlugin.EffectiveWorkingPluginId)) + { + this.chat.PrintError(Loc.Localize("PluginCommandsNotInDefaultProfile", "Plugin \"{0}\" is in a collection and can't be managed through commands. Manage the collection instead.") + .Format(targetPlugin.Name)); + } + + return targetPlugin; + } + + private abstract record Target; + + private record PluginTarget(Guid WorkingPluginId) : Target; + + private record ProfileTarget(string ProfileName) : Target; }