diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 371d3add2d..f2c8cbc5a6 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -25,9 +25,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 5c2378f689..4df38d6dfc 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -249,7 +249,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui. /// public bool IsDocking { get; set; } - + /// /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. /// This setting is effected by the in-game "System Sounds" option and volume. @@ -484,10 +484,15 @@ public string EffectiveLanguage public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null; /// - /// Gets or sets a value indicating whether or not users should be notified regularly about pending updates. + /// Gets or sets a value indicating whether users should be notified regularly about pending updates. /// public bool CheckPeriodicallyForUpdates { get; set; } = true; + /// + /// Gets or sets a value indicating whether users should be notified about updates in chat. + /// + public bool SendUpdateNotificationToChat { get; set; } = false; + /// /// Load a configuration from the provided path. /// @@ -504,7 +509,7 @@ public static DalamudConfiguration Load(string path, ReliableFileStorage fs) { deserialized = JsonConvert.DeserializeObject(text, SerializerSettings); - + // If this reads as null, the file was empty, that's no good if (deserialized == null) throw new Exception("Read config was null."); @@ -530,7 +535,7 @@ public static DalamudConfiguration Load(string path, ReliableFileStorage fs) { Log.Error(e, "Failed to set defaults for DalamudConfiguration"); } - + return deserialized; } @@ -549,7 +554,7 @@ public void ForceSave() { this.Save(); } - + /// void IInternalDisposableService.DisposeService() { @@ -595,14 +600,14 @@ private void SetDefaults() this.ReduceMotions = winAnimEnabled == 0; } } - + // Migrate old auto-update setting to new auto-update behavior this.AutoUpdateBehavior ??= this.AutoUpdatePlugins ? Plugin.Internal.AutoUpdate.AutoUpdateBehavior.UpdateAll : Plugin.Internal.AutoUpdate.AutoUpdateBehavior.OnlyNotify; #pragma warning restore CS0618 } - + private void Save() { ThreadSafety.AssertMainThread(); diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index f51f7e08b9..86aba30b0c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -5,8 +5,8 @@ - 11.0.2.0 XIV Launcher addon framework + 11.0.4.0 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) @@ -67,14 +67,14 @@ - - + + all - + diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs index d2b1b4a36d..10f177590d 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs @@ -272,15 +272,14 @@ public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector4 /// Width. public static float GetIconButtonWithTextWidth(FontAwesomeIcon icon, string text) { + Vector2 iconSize; using (ImRaii.PushFont(UiBuilder.IconFont)) { - var iconSize = ImGui.CalcTextSize(icon.ToIconString()); - - var textSize = ImGui.CalcTextSize(text); - - var iconPadding = 3 * ImGuiHelpers.GlobalScale; - - return iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding; + iconSize = ImGui.CalcTextSize(icon.ToIconString()); } + + var textSize = ImGui.CalcTextSize(text); + var iconPadding = 3 * ImGuiHelpers.GlobalScale; + return iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding; } } diff --git a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs index f0ce6bc824..3d31bbda69 100644 --- a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs +++ b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs @@ -32,19 +32,21 @@ internal static uint DrawPluginPicker(string id, ref string pickerSearch, Action var pm = Service.GetNullable(); if (pm == null) return 0; - + var addPluginToProfilePopupId = ImGui.GetID(id); using var popup = ImRaii.Popup(id); if (popup.Success) { var width = ImGuiHelpers.GlobalScale * 300; - + ImGui.SetNextItemWidth(width); ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref pickerSearch, 255); var currentSearchString = pickerSearch; - if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80))) + + using var listBox = ImRaii.ListBox("###pluginPicker", new Vector2(width, width - 80)); + if (listBox.Success) { // TODO: Plugin searching should be abstracted... installer and this should use the same search var plugins = pm.InstalledPlugins.Where( @@ -53,19 +55,15 @@ internal static uint DrawPluginPicker(string id, ref string pickerSearch, Action currentSearchString, StringComparison.InvariantCultureIgnoreCase))) .Where(pluginFiltered ?? (_ => true)); - + foreach (var plugin in plugins) { - using var disabled2 = - ImRaii.Disabled(pluginDisabled(plugin)); - + using var disabled2 = ImRaii.Disabled(pluginDisabled(plugin)); if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}")) { onClicked(plugin); } } - - ImGui.EndListBox(); } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 95315dbd3f..ddb89d38c0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -625,13 +625,13 @@ private static class Locs 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(ProfileCommandHandler.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(ProfileCommandHandler.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(ProfileCommandHandler.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/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs index 77c79c96d4..9356131ad3 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -23,10 +23,11 @@ public class SettingsTabAutoUpdates : SettingsTab { private AutoUpdateBehavior behavior; private bool checkPeriodically; + private bool chatNotification; private string pickerSearch = string.Empty; private List autoUpdatePreferences = []; - - public override SettingsEntry[] Entries { get; } = Array.Empty(); + + public override SettingsEntry[] Entries { get; } = []; public override string Title => Loc.Localize("DalamudSettingsAutoUpdates", "Auto-Updates"); @@ -36,15 +37,15 @@ public override void Draw() "Dalamud can update your plugins automatically, making sure that you always " + "have the newest features and bug fixes. You can choose when and how auto-updates are run here.")); ImGuiHelpers.ScaledDummy(2); - + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer1", "You can always update your plugins manually by clicking the update button in the plugin list. " + "You can also opt into updates for specific plugins by right-clicking them and selecting \"Always auto-update\".")); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer2", "Dalamud will only notify you about updates while you are idle.")); - + ImGuiHelpers.ScaledDummy(8); - + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior", "When the game starts...")); var behaviorInt = (int)this.behavior; @@ -62,20 +63,21 @@ public override void Draw() "These updates are not reviewed by the Dalamud team and may contain malicious code."); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudOrange, warning); } - + ImGuiHelpers.ScaledDummy(8); - + + ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateChatMessage", "Show notification about updates available in chat"), ref this.chatNotification); ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint", "Plugins won't update automatically after startup, you will only receive a notification while you are not actively playing.")); - + ImGuiHelpers.ScaledDummy(5); ImGui.Separator(); ImGuiHelpers.ScaledDummy(5); - + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOptedIn", "Per-plugin overrides")); - + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOverrideHint", "Here, you can choose to receive or not to receive updates for specific plugins. " + "This will override the settings above for the selected plugins.")); @@ -83,25 +85,25 @@ public override void Draw() if (this.autoUpdatePreferences.Count == 0) { ImGuiHelpers.ScaledDummy(20); - + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) { ImGuiHelpers.CenteredText(Loc.Localize("DalamudSettingsAutoUpdateOptedInHint2", "You don't have auto-update rules for any plugins.")); } - + ImGuiHelpers.ScaledDummy(2); } else { ImGuiHelpers.ScaledDummy(5); - + var pic = Service.Get(); var windowSize = ImGui.GetWindowSize(); var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale; Guid? wantRemovePluginGuid = null; - + foreach (var preference in this.autoUpdatePreferences) { var pmPlugin = Service.Get().InstalledPlugins @@ -120,11 +122,12 @@ public override void Draw() if (pmPlugin.IsDev) { ImGui.SetCursorPos(cursorBeforeIcon); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.7f); - ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); - ImGui.PopStyleVar(); + using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.7f)) + { + ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); + } } - + ImGui.SameLine(); var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}"; @@ -147,7 +150,7 @@ public override void Draw() ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); ImGui.TextUnformatted(text); - + ImGui.SetCursorPos(before); } @@ -166,19 +169,18 @@ string OptKindToString(AutoUpdatePreference.OptKind kind) } ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 250); - if (ImGui.BeginCombo( - $"###autoUpdateBehavior{preference.WorkingPluginId}", - OptKindToString(preference.Kind))) + using (var combo = ImRaii.Combo($"###autoUpdateBehavior{preference.WorkingPluginId}", OptKindToString(preference.Kind))) { - foreach (var kind in Enum.GetValues()) + if (combo.Success) { - if (ImGui.Selectable(OptKindToString(kind))) + foreach (var kind in Enum.GetValues()) { - preference.Kind = kind; + if (ImGui.Selectable(OptKindToString(kind))) + { + preference.Kind = kind; + } } } - - ImGui.EndCombo(); } ImGui.SameLine(); @@ -193,7 +195,7 @@ string OptKindToString(AutoUpdatePreference.OptKind kind) if (ImGui.IsItemHovered()) ImGui.SetTooltip(Loc.Localize("DalamudSettingsAutoUpdateOptInRemove", "Remove this override")); } - + if (wantRemovePluginGuid != null) { this.autoUpdatePreferences.RemoveAll(x => x.WorkingPluginId == wantRemovePluginGuid); @@ -205,19 +207,19 @@ void OnPluginPicked(LocalPlugin plugin) var id = plugin.EffectiveWorkingPluginId; if (id == Guid.Empty) throw new InvalidOperationException("Plugin ID is empty."); - + this.autoUpdatePreferences.Add(new AutoUpdatePreference(id)); } - + bool IsPluginDisabled(LocalPlugin plugin) => this.autoUpdatePreferences.Any(x => x.WorkingPluginId == plugin.EffectiveWorkingPluginId); - + bool IsPluginFiltered(LocalPlugin plugin) => !plugin.IsDev; - + var pickerId = DalamudComponents.DrawPluginPicker( "###autoUpdatePicker", ref this.pickerSearch, OnPluginPicked, IsPluginDisabled, IsPluginFiltered); - + const FontAwesomeIcon addButtonIcon = FontAwesomeIcon.Plus; var addButtonText = Loc.Localize("DalamudSettingsAutoUpdateOptInAdd", "Add new override"); ImGuiHelpers.CenterCursorFor(ImGuiComponents.GetIconButtonWithTextWidth(addButtonIcon, addButtonText)); @@ -235,20 +237,22 @@ public override void Load() var configuration = Service.Get(); this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None; + this.chatNotification = configuration.SendUpdateNotificationToChat; this.checkPeriodically = configuration.CheckPeriodicallyForUpdates; this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences; - + base.Load(); } public override void Save() { var configuration = Service.Get(); - + configuration.AutoUpdateBehavior = this.behavior; + configuration.SendUpdateNotificationToChat = this.chatNotification; configuration.CheckPeriodicallyForUpdates = this.checkPeriodically; configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences; - + base.Save(); } } diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index c25ec4ee41..99850ddb49 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -9,6 +9,10 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.EventArgs; @@ -31,17 +35,17 @@ namespace Dalamud.Plugin.Internal.AutoUpdate; internal class AutoUpdateManager : IServiceType { private static readonly ModuleLog Log = new("AUTOUPDATE"); - + /// /// Time we should wait after login to update. /// private static readonly TimeSpan UpdateTimeAfterLogin = TimeSpan.FromSeconds(20); - + /// /// Time we should wait between scheduled update checks. /// private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(2); - + /// /// Time we should wait between scheduled update checks if the user has dismissed the notification, /// instead of updating. We don't want to spam the user with notifications. @@ -56,28 +60,30 @@ internal class AutoUpdateManager : IServiceType [ServiceManager.ServiceDependency] private readonly PluginManager pluginManager = Service.Get(); - + [ServiceManager.ServiceDependency] private readonly DalamudConfiguration config = Service.Get(); - + [ServiceManager.ServiceDependency] private readonly NotificationManager notificationManager = Service.Get(); - + [ServiceManager.ServiceDependency] private readonly DalamudInterface dalamudInterface = Service.Get(); - + private readonly IConsoleVariable isDryRun; - + private DateTime? loginTime; private DateTime? nextUpdateCheckTime; private DateTime? unblockedSince; - + private bool hasStartedInitialUpdateThisSession; private IActiveNotification? updateNotification; - + private Task? autoUpdateTask; - + + private readonly Task openInstallerWindowLink; + /// /// Initializes a new instance of the class. /// @@ -92,7 +98,18 @@ public AutoUpdateManager(ConsoleManager console) t.Result.Logout += (int type, int code) => this.OnLogout(); }); Service.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; }); - + + this.openInstallerWindowLink = + Service.GetAsync().ContinueWith( + chatGuiTask => chatGuiTask.Result.AddChatLinkHandler( + "Dalamud", + 1001, + (_, _) => + { + Service.GetNullable()?.OpenPluginInstallerTo(PluginInstallerOpenKind.InstalledPlugins); + })); + + this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", false); console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () => { @@ -106,36 +123,36 @@ public AutoUpdateManager(ConsoleManager console) return true; }); } - + private enum UpdateListingRestriction { Unrestricted, AllowNone, AllowMainRepo, } - + /// /// Gets a value indicating whether or not auto-updates have already completed this session. /// public bool IsAutoUpdateComplete { get; private set; } - + /// /// Gets the time of the next scheduled update check. /// public DateTime? NextUpdateCheckTime => this.nextUpdateCheckTime; - + /// /// Gets the time the auto-update was unblocked. /// public DateTime? UnblockedSince => this.unblockedSince; - + private static UpdateListingRestriction DecideUpdateListingRestriction(AutoUpdateBehavior behavior) { return behavior switch { // We don't generally allow any updates in this mode, but specific opt-ins. AutoUpdateBehavior.None => UpdateListingRestriction.AllowNone, - + // If we're only notifying, I guess it's fine to list all plugins. AutoUpdateBehavior.OnlyNotify => UpdateListingRestriction.Unrestricted, @@ -144,7 +161,7 @@ private static UpdateListingRestriction DecideUpdateListingRestriction(AutoUpdat _ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null), }; } - + private static void DrawOpenInstallerNotificationButton(bool primary, PluginInstallerOpenKind kind, IActiveNotification notification) { if (primary ? @@ -179,7 +196,7 @@ private void OnUpdate(IFramework framework) this.updateNotification = null; } } - + // If we're blocked, we don't do anything. if (!isUnblocked) return; @@ -199,16 +216,16 @@ private void OnUpdate(IFramework framework) if (!this.hasStartedInitialUpdateThisSession && DateTime.Now > this.loginTime.Value.Add(UpdateTimeAfterLogin)) { this.hasStartedInitialUpdateThisSession = true; - + var currentlyUpdatablePlugins = this.GetAvailablePluginUpdates(DecideUpdateListingRestriction(behavior)); if (currentlyUpdatablePlugins.Count == 0) { this.IsAutoUpdateComplete = true; this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks; - + return; } - + // TODO: This is not 100% what we want... Plugins that are opted-in should be updated regardless of the behavior, // and we should show a notification for the others afterwards. if (behavior == AutoUpdateBehavior.OnlyNotify) @@ -241,6 +258,7 @@ private void OnUpdate(IFramework framework) Log.Error(t.Exception!, "Failed to reload plugin masters for auto-update"); } + Log.Verbose($"Available Updates: {string.Join(", ", this.pluginManager.UpdatablePlugins.Select(s => s.UpdateManifest.InternalName))}"); var updatable = this.GetAvailablePluginUpdates( DecideUpdateListingRestriction(behavior)); @@ -252,7 +270,7 @@ private void OnUpdate(IFramework framework) { this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks; Log.Verbose( - "Auto update found nothing to do, next update at {Time}", + "Auto update found nothing to do, next update at {Time}", this.nextUpdateCheckTime); } }); @@ -263,13 +281,13 @@ private IActiveNotification GetBaseNotification(Notification notification) { if (this.updateNotification != null) throw new InvalidOperationException("Already showing a notification"); - + this.updateNotification = this.notificationManager.AddNotification(notification); this.updateNotification.Dismiss += _ => { this.updateNotification = null; - + // Schedule the next update opportunistically for when this closes. this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks; }; @@ -291,7 +309,7 @@ private void KickOffAutoUpdates(ICollection updatablePlug { Log.Warning("Auto-update task was canceled"); } - + this.autoUpdateTask = null; this.IsAutoUpdateComplete = true; }); @@ -321,20 +339,20 @@ private async Task RunAutoUpdates(ICollection updatablePl notification.Content = Locs.NotificationContentUpdating(updateProgress.CurrentPluginManifest.Name); notification.Progress = (float)updateProgress.PluginsProcessed / updateProgress.TotalPlugins; }; - + var pluginStates = (await this.pluginManager.UpdatePluginsAsync(updatablePlugins, this.isDryRun.Value, true, progress)).ToList(); this.pluginManager.PrintUpdatedPlugins(pluginStates, Loc.Localize("DalamudPluginAutoUpdate", "The following plugins were auto-updated:")); notification.Progress = 1; notification.UserDismissable = true; notification.HardExpiry = DateTime.Now.AddSeconds(30); - + notification.DrawActions += _ => { ImGuiHelpers.ScaledDummy(2); DrawOpenInstallerNotificationButton(true, PluginInstallerOpenKind.InstalledPlugins, notification); }; - + // Update the notification to show the final state if (pluginStates.All(x => x.Status == PluginUpdateStatus.StatusKind.Success)) { @@ -342,7 +360,7 @@ private async Task RunAutoUpdates(ICollection updatablePl // Janky way to make sure the notification does not change before it's minimized... await Task.Delay(500); - + notification.Title = Locs.NotificationTitleUpdatesSuccessful; notification.MinimizedText = Locs.NotificationContentUpdatesSuccessfulMinimized; notification.Type = NotificationType.Success; @@ -354,11 +372,11 @@ private async Task RunAutoUpdates(ICollection updatablePl notification.MinimizedText = Locs.NotificationContentUpdatesFailedMinimized; notification.Type = NotificationType.Error; notification.Content = Locs.NotificationContentUpdatesFailed; - + var failedPlugins = pluginStates .Where(x => x.Status != PluginUpdateStatus.StatusKind.Success) .Select(x => x.Name).ToList(); - + notification.Content += "\n" + Locs.NotificationContentFailedPlugins(failedPlugins); } } @@ -367,7 +385,7 @@ private void NotifyUpdatesAreAvailable(ICollection updata { if (updatablePlugins.Count == 0) return; - + var notification = this.GetBaseNotification(new Notification { Title = Locs.NotificationTitleUpdatesAvailable, @@ -400,16 +418,44 @@ void DrawNotificationContent(INotificationDrawArgs args) notification.Dismiss += args => { if (args.Reason != NotificationDismissReason.Manual) return; - + this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecksIfDismissed; Log.Verbose("User dismissed update notification, next check at {Time}", this.nextUpdateCheckTime); }; + + // Send out a chat message only if the user requested so + if (!this.config.SendUpdateNotificationToChat) + return; + + var chatGui = Service.GetNullable(); + if (chatGui == null) + { + Log.Verbose("Unable to get chat gui, discard notification for chat."); + return; + } + + chatGui.Print(new XivChatEntry + { + Message = new SeString(new List + { + new TextPayload(Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count)), + new TextPayload(" ["), + new UIForegroundPayload(500), + this.openInstallerWindowLink.Result, + new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")), + RawPayload.LinkTerminator, + new UIForegroundPayload(0), + new TextPayload("]"), + }), + + Type = XivChatType.Urgent, + }); } - + private List GetAvailablePluginUpdates(UpdateListingRestriction restriction) { var optIns = this.config.PluginAutoUpdatePreferences.ToArray(); - + // Get all of our updatable plugins and do some initial filtering that must apply to all plugins. var updateablePlugins = this.pluginManager.UpdatablePlugins .Where( @@ -423,14 +469,14 @@ private List GetAvailablePluginUpdates(UpdateListingRestr bool FilterPlugin(AvailablePluginUpdate availablePluginUpdate) { var optIn = optIns.FirstOrDefault(x => x.WorkingPluginId == availablePluginUpdate.InstalledPlugin.EffectiveWorkingPluginId); - + // If this is an opt-out, we don't update. if (optIn is { Kind: AutoUpdatePreference.OptKind.NeverUpdate }) return false; if (restriction == UpdateListingRestriction.AllowNone && optIn is not { Kind: AutoUpdatePreference.OptKind.AlwaysUpdate }) return false; - + if (restriction == UpdateListingRestriction.AllowMainRepo && availablePluginUpdate.InstalledPlugin.IsThirdParty) return false; @@ -442,7 +488,7 @@ private void OnLogin() { this.loginTime = DateTime.Now; } - + private void OnLogout() { this.loginTime = null; @@ -452,7 +498,7 @@ private bool CanUpdateOrNag() { var condition = Service.Get(); return this.IsPluginManagerReady() && - !this.dalamudInterface.IsPluginInstallerOpen && + !this.dalamudInterface.IsPluginInstallerOpen && condition.OnlyAny(ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, @@ -469,21 +515,21 @@ private static class Locs public static string NotificationButtonOpenPluginInstaller => Loc.Localize("AutoUpdateOpenPluginInstaller", "Open installer"); public static string NotificationButtonUpdate => Loc.Localize("AutoUpdateUpdate", "Update"); - + public static string NotificationTitleUpdatesAvailable => Loc.Localize("AutoUpdateUpdatesAvailable", "Updates available!"); - + public static string NotificationTitleUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessful", "Updates successful!"); - + public static string NotificationTitleUpdatingPlugins => Loc.Localize("AutoUpdateUpdatingPlugins", "Updating plugins..."); - + public static string NotificationTitleUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailed", "Updates failed!"); - + public static string NotificationContentUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessfulContent", "All plugins have been updated successfully."); - + public static string NotificationContentUpdatesSuccessfulMinimized => Loc.Localize("AutoUpdateUpdatesSuccessfulContentMinimized", "Plugins updated successfully."); - + public static string NotificationContentUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailedContent", "Some plugins failed to update. Please check the plugin installer for more information."); - + public static string NotificationContentUpdatesFailedMinimized => Loc.Localize("AutoUpdateUpdatesFailedContentMinimized", "Plugins failed to update."); public static string NotificationContentUpdatesAvailable(ICollection updatablePlugins) @@ -497,20 +543,20 @@ public static string NotificationContentUpdatesAvailable(ICollection x.InstalledPlugin.Manifest.Name)); - + public static string NotificationContentUpdatesAvailableMinimized(int numUpdates) => numUpdates == 1 ? - Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") : + Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") : string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedPlural", "{0} plugin updates available"), numUpdates); - + public static string NotificationContentPreparingToUpdate(int numPlugins) => numPlugins == 1 ? - Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") : + Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") : string.Format(Loc.Localize("AutoUpdatePreparingToUpdatePlural", "Preparing to update {0} plugins..."), numPlugins); - + public static string NotificationContentUpdating(string name) => string.Format(Loc.Localize("AutoUpdateUpdating", "Updating {0}..."), name); - + public static string NotificationContentFailedPlugins(IEnumerable failedPlugins) => string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugin(s): {0}"), string.Join(", ", failedPlugins)); } diff --git a/Dalamud/Plugin/Internal/Exceptions/InternalPluginStateException.cs b/Dalamud/Plugin/Internal/Exceptions/InternalPluginStateException.cs new file mode 100644 index 0000000000..03e37afcfa --- /dev/null +++ b/Dalamud/Plugin/Internal/Exceptions/InternalPluginStateException.cs @@ -0,0 +1,16 @@ +namespace Dalamud.Plugin.Internal.Exceptions; + +/// +/// An exception to be thrown when policy blocks a plugin from loading. +/// +internal class InternalPluginStateException : InvalidPluginOperationException +{ + /// + /// Initializes a new instance of the class. + /// + /// The message to associate with this exception. + public InternalPluginStateException(string message) + : base(message) + { + } +} diff --git a/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs new file mode 100644 index 0000000000..ad5aad286d --- /dev/null +++ b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs @@ -0,0 +1,388 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +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; + +namespace Dalamud.Plugin.Internal.Profiles; + +/// +/// Service responsible for profile-related chat commands. +/// +[ServiceManager.EarlyLoadedService] +internal class PluginManagementCommandHandler : IInternalDisposableService +{ +#pragma warning disable SA1600 + 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 = 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<(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, + PluginManager pluginManager, + ChatGui chat, + Framework framework) + { + this.cmd = cmd; + this.profileManager = profileManager; + this.pluginManager = pluginManager; + this.chat = chat; + this.framework = framework; + + this.cmd.AddHandler(CommandEnableProfile, new CommandInfo(this.OnEnableProfile) + { + HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""), + ShowInHelp = true, + }); + + this.cmd.AddHandler(CommandDisableProfile, new CommandInfo(this.OnDisableProfile) + { + HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""), + ShowInHelp = true, + }); + + this.cmd.AddHandler(CommandToggleProfile, new CommandInfo(this.OnToggleProfile) + { + HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""), + ShowInHelp = true, + }); + + this.cmd.AddHandler(LegacyCommandEnable, new CommandInfo(this.OnEnableProfile) + { + ShowInHelp = false, + }); + + this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile) + { + 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 PluginCommandOperation + { + Enable, + Disable, + Toggle, + } + + /// + void IInternalDisposableService.DisposeService() + { + 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 HandleProfileOperation(string profileName, PluginCommandOperation operation) + { + var profile = this.profileManager.Profiles.FirstOrDefault( + x => x.Name == profileName); + if (profile == null || profile.IsDefaultProfile) + return; + + switch (operation) + { + 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); + } + + this.chat.Print( + profile.IsEnabled + ? Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name) + : Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name)); + + Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t => + { + 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.")); + } + }); + } + + private bool HandlePluginOperation(Guid workingPluginId, PluginCommandOperation operation) + { + var plugin = this.pluginManager.InstalledPlugins.FirstOrDefault(x => x.EffectiveWorkingPluginId == workingPluginId); + if (plugin == null) + return true; + + 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) + { + 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.ValidateProfileName(arguments); + if (name == null) + return; + + 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.ValidateProfileName(arguments); + if (name == null) + return; + + 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.ValidateProfileName(arguments); + if (name == null) + return; + + 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? ValidateProfileName(string arguments) + { + var name = arguments.Replace("\"", string.Empty); + if (this.profileManager.Profiles.All(x => x.Name != 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; +} diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs deleted file mode 100644 index 7b7b4cfd0c..0000000000 --- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using CheapLoc; -using Dalamud.Game; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using Dalamud.Plugin.Services; -using Dalamud.Utility; -using Serilog; - -namespace Dalamud.Plugin.Internal.Profiles; - -/// -/// Service responsible for profile-related chat commands. -/// -[ServiceManager.EarlyLoadedService] -internal class ProfileCommandHandler : IInternalDisposableService -{ -#pragma warning disable SA1600 - public const string CommandEnable = "/xlenablecollection"; - public const string CommandDisable = "/xldisablecollection"; - public const string CommandToggle = "/xltogglecollection"; -#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 readonly CommandManager cmd; - private readonly ProfileManager profileManager; - private readonly ChatGui chat; - private readonly Framework framework; - - private List<(string, ProfileOp)> queue = new(); - - /// - /// Initializes a new instance of the class. - /// - /// Command handler. - /// Profile manager. - /// Chat handler. - /// Framework. - [ServiceManager.ServiceConstructor] - public ProfileCommandHandler(CommandManager cmd, ProfileManager profileManager, ChatGui chat, Framework framework) - { - this.cmd = cmd; - this.profileManager = profileManager; - this.chat = chat; - this.framework = framework; - - this.cmd.AddHandler(CommandEnable, 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) - { - HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""), - ShowInHelp = true, - }); - - this.cmd.AddHandler(CommandToggle, new CommandInfo(this.OnToggleProfile) - { - HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""), - ShowInHelp = true, - }); - - this.cmd.AddHandler(LegacyCommandEnable, new CommandInfo(this.OnEnableProfile) - { - ShowInHelp = false, - }); - - this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile) - { - ShowInHelp = true, - }); - - this.cmd.AddHandler(LegacyCommandToggle, new CommandInfo(this.OnToggleProfile) - { - ShowInHelp = true, - }); - - this.framework.Update += this.FrameworkOnUpdate; - } - - private enum ProfileOp - { - Enable, - Disable, - Toggle, - } - - /// - void IInternalDisposableService.DisposeService() - { - this.cmd.RemoveHandler(CommandEnable); - this.cmd.RemoveHandler(CommandDisable); - this.cmd.RemoveHandler(CommandToggle); - this.cmd.RemoveHandler(LegacyCommandEnable); - this.cmd.RemoveHandler(LegacyCommandDisable); - this.cmd.RemoveHandler(LegacyCommandToggle); - - this.framework.Update += this.FrameworkOnUpdate; - } - - private void FrameworkOnUpdate(IFramework framework1) - { - if (this.profileManager.IsBusy) - return; - - if (this.queue.Count > 0) - { - var op = this.queue[0]; - this.queue.RemoveAt(0); - - var profile = this.profileManager.Profiles.FirstOrDefault(x => x.Name == op.Item1); - if (profile == null || profile.IsDefaultProfile) - return; - - switch (op.Item2) - { - 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)); - } - else - { - this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name)); - } - - Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t => - { - 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.")); - } - }); - } - } - - private void OnEnableProfile(string command, string arguments) - { - var name = this.ValidateName(arguments); - if (name == null) - return; - - this.queue = this.queue.Where(x => x.Item1 != name).ToList(); - this.queue.Add((name, ProfileOp.Enable)); - } - - private void OnDisableProfile(string command, string arguments) - { - var name = this.ValidateName(arguments); - if (name == null) - return; - - this.queue = this.queue.Where(x => x.Item1 != name).ToList(); - this.queue.Add((name, ProfileOp.Disable)); - } - - private void OnToggleProfile(string command, string arguments) - { - var name = this.ValidateName(arguments); - if (name == null) - return; - - this.queue.Add((name, ProfileOp.Toggle)); - } - - private string? ValidateName(string arguments) - { - var name = arguments.Replace("\"", string.Empty); - if (this.profileManager.Profiles.All(x => x.Name != name)) - { - this.chat.PrintError($"No collection like \"{name}\"."); - return null; - } - - return name; - } -} diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 59f6b23c19..ed3a94994f 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -281,7 +281,7 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false) case PluginState.Unloaded: if (this.instance is not null) { - throw new InvalidPluginOperationException( + throw new InternalPluginStateException( "Plugin should have been unloaded but instance is not cleared"); } @@ -413,7 +413,9 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false) } catch (Exception ex) { - this.State = PluginState.LoadError; + // These are "user errors", we don't want to mark the plugin as failed + if (ex is not InvalidPluginOperationException) + this.State = PluginState.LoadError; // If a precondition fails, don't record it as an error, as it isn't really. if (ex is PluginPreconditionFailedException) @@ -476,7 +478,10 @@ public async Task UnloadAsync(PluginLoaderDisposalMode disposalMode = PluginLoad } catch (Exception ex) { - this.State = PluginState.UnloadError; + // These are "user errors", we don't want to mark the plugin as failed + if (ex is not InvalidPluginOperationException) + this.State = PluginState.UnloadError; + Log.Error(ex, "Error while unloading {PluginName}", this.InternalName); throw; diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index df03181ccb..cc98a564d0 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit df03181ccbbbfead3db116b59359dae4a31cb07d +Subproject commit cc98a564d0787813d4be082bf75f5bb98e0ed12f diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 0000000000..8863a12149 --- /dev/null +++ b/release.ps1 @@ -0,0 +1,31 @@ +param( + [string]$VersionString +) + +if (-not $VersionString) { + Write-Error "Version string is required as the first argument." + exit 1 +} + +$csprojPath = "Dalamud/Dalamud.csproj" + +if (-not (Test-Path $csprojPath)) { + Write-Error "Cannot find Dalamud.csproj at the specified path." + exit 1 +} + +# Update the version in the csproj file +(Get-Content $csprojPath) -replace '.*?', "$VersionString" | Set-Content $csprojPath + +# Commit the change +git add $csprojPath +git commit -m "build: $VersionString" + +# Get the current branch +$currentBranch = git rev-parse --abbrev-ref HEAD + +# Create a tag +git tag -a -m "v$VersionString" $VersionString + +# Push atomically +git push origin $currentBranch $VersionString \ No newline at end of file