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;
}