From 993819f8cbe74991612d6eae4cbec1128da1468a Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 9 Jun 2024 14:47:23 +0200 Subject: [PATCH 01/23] console: add bearable autocompletion --- .../Internal/Windows/ConsoleWindow.cs | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 51ab7404a..e6d7abd9c 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -79,6 +79,9 @@ internal class ConsoleWindow : Window, IDisposable private int historyPos; private int copyStart = -1; + private string? completionZipText = null; + private int completionTabIdx = 0; + private IActiveNotification? prevCopyNotification; /// Initializes a new instance of the class. @@ -315,7 +318,7 @@ internal class ConsoleWindow : Window, IDisposable ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | - ImGuiInputTextFlags.CallbackHistory, + ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.CallbackEdit, this.CommandInputCallback)) { this.ProcessCommand(); @@ -832,6 +835,11 @@ internal class ConsoleWindow : Window, IDisposable switch (data->EventFlag) { + case ImGuiInputTextFlags.CallbackEdit: + this.completionZipText = null; + this.completionTabIdx = 0; + break; + case ImGuiInputTextFlags.CallbackCompletion: var textBytes = new byte[data->BufTextLen]; Marshal.Copy((IntPtr)data->Buf, textBytes, 0, data->BufTextLen); @@ -842,22 +850,47 @@ internal class ConsoleWindow : Window, IDisposable // We can't do any completion for parameters at the moment since it just calls into CommandHandler if (words.Length > 1) return 0; + + var wordToComplete = words[0]; + if (wordToComplete.IsNullOrWhitespace()) + return 0; + + if (this.completionZipText is not null) + wordToComplete = this.completionZipText; // TODO: Improve this, add partial completion, arguments, description, etc. // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484 var candidates = Service.Get().Entries - .Where(x => x.Key.StartsWith(words[0])) + .Where(x => x.Key.StartsWith(wordToComplete)) .Select(x => x.Key); candidates = candidates.Union( Service.Get().Commands - .Where(x => x.Key.StartsWith(words[0])).Select(x => x.Key)); + .Where(x => x.Key.StartsWith(wordToComplete)).Select(x => x.Key)) + .ToArray(); - var enumerable = candidates as string[] ?? candidates.ToArray(); - if (enumerable.Length != 0) + if (candidates.Any()) { - ptr.DeleteChars(0, ptr.BufTextLen); - ptr.InsertChars(0, enumerable[0]); + string? toComplete = null; + if (this.completionZipText == null) + { + // Find the "common" prefix of all matches + toComplete = candidates.Aggregate( + (prefix, candidate) => string.Concat(prefix.Zip(candidate, (a, b) => a == b ? a : '\0'))); + + this.completionZipText = toComplete; + } + else + { + toComplete = candidates.ElementAt(this.completionTabIdx); + this.completionTabIdx = (this.completionTabIdx + 1) % candidates.Count(); + } + + if (toComplete != null) + { + ptr.DeleteChars(0, ptr.BufTextLen); + ptr.InsertChars(0, toComplete); + } } break; From 2d5c4ed7dc9bf99abdc245a55d6fdeb2512ab9dc Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 9 Jun 2024 21:32:23 +0200 Subject: [PATCH 02/23] console: print variable contents, flip bools, print command on enter --- Dalamud/Console/ConsoleManager.cs | 26 ++++++++++++++----- .../Internal/Windows/ConsoleWindow.cs | 1 + 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs index 6600069c2..4112cde2a 100644 --- a/Dalamud/Console/ConsoleManager.cs +++ b/Dalamud/Console/ConsoleManager.cs @@ -270,7 +270,9 @@ internal partial class ConsoleManager : IServiceType for (var i = parsedArguments.Count; i < entry.ValidArguments.Count; i++) { var argument = entry.ValidArguments[i]; - if (argument.DefaultValue == null) + + // If the default value is DBNull, we need to error out as that means it was not specified + if (argument.DefaultValue == DBNull.Value) { Log.Error("Not enough arguments for command {CommandName}", entryName); PrintUsage(entry); @@ -382,11 +384,8 @@ internal partial class ConsoleManager : IServiceType /// The default value to use if none is specified. /// An instance. /// Thrown if the given type cannot be handled by the console system. - protected static ArgumentInfo TypeToArgument(Type type, object? defaultValue = null) + protected static ArgumentInfo TypeToArgument(Type type, object? defaultValue) { - // If the default value is DBNull, we want to treat it as null - defaultValue = defaultValue == DBNull.Value ? null : defaultValue; - if (type == typeof(string)) return new ArgumentInfo(ConsoleArgumentType.String, defaultValue); @@ -490,7 +489,7 @@ internal partial class ConsoleManager : IServiceType public ConsoleVariable(string name, string description) : base(name, description) { - this.ValidArguments = new List { TypeToArgument(typeof(T)) }; + this.ValidArguments = new List { TypeToArgument(typeof(T), null) }; } /// @@ -500,7 +499,20 @@ internal partial class ConsoleManager : IServiceType public override bool Invoke(IEnumerable arguments) { var first = arguments.FirstOrDefault(); - if (first == null || first.GetType() != typeof(T)) + + if (first == null) + { + // Invert the value if it's a boolean + if (this.Value is bool boolValue) + { + this.Value = (T)(object)!boolValue; + } + + Log.WriteLog(LogEventLevel.Information, "{VariableName} = {VariableValue}", null, this.Name, this.Value); + return true; + } + + if (first.GetType() != typeof(T)) throw new ArgumentException($"Console variable must be set with an argument of type {typeof(T).Name}."); this.Value = (T)first; diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index e6d7abd9c..85c7a7380 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -321,6 +321,7 @@ internal class ConsoleWindow : Window, IDisposable ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.CallbackEdit, this.CommandInputCallback)) { + this.newLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), []))); this.ProcessCommand(); getFocus = true; } From 1133fcfb9dd199fe652857289840a925661b73ab Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 9 Jun 2024 21:42:30 +0200 Subject: [PATCH 03/23] console: persist log command buffer in config --- .../Internal/Windows/ConsoleWindow.cs | 69 +++++++++---------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 85c7a7380..ebb89fbee 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -38,6 +38,7 @@ internal class ConsoleWindow : Window, IDisposable { private const int LogLinesMinimum = 100; private const int LogLinesMaximum = 1000000; + private const int HistorySize = 50; // Only this field may be touched from any thread. private readonly ConcurrentQueue<(string Line, LogEvent LogEvent)> newLogEntries; @@ -45,9 +46,10 @@ internal class ConsoleWindow : Window, IDisposable // Fields below should be touched only from the main thread. private readonly RollingList logText; private readonly RollingList filteredLogEntries; - - private readonly List history = new(); + private readonly List pluginFilters = new(); + + private readonly DalamudConfiguration configuration; private int newRolledLines; private bool pendingRefilter; @@ -89,6 +91,8 @@ internal class ConsoleWindow : Window, IDisposable public ConsoleWindow(DalamudConfiguration configuration) : base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { + this.configuration = configuration; + this.autoScroll = configuration.LogAutoScroll; this.autoOpen = configuration.LogOpenAtStartup; SerilogEventSink.Instance.LogLine += this.OnLogLine; @@ -115,7 +119,7 @@ internal class ConsoleWindow : Window, IDisposable this.logText = new(limit); this.filteredLogEntries = new(limit); - configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved; + this.configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved; unsafe { @@ -134,7 +138,7 @@ internal class ConsoleWindow : Window, IDisposable public void Dispose() { SerilogEventSink.Instance.LogLine -= this.OnLogLine; - Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; + this.configuration.DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; if (Service.GetNullable() is { } framework) framework.Update -= this.FrameworkOnUpdate; @@ -465,8 +469,6 @@ internal class ConsoleWindow : Window, IDisposable private void DrawOptionsToolbar() { - var configuration = Service.Get(); - ImGui.PushItemWidth(150.0f * ImGuiHelpers.GlobalScale); if (ImGui.BeginCombo("##log_level", $"{EntryPoint.LogLevelSwitch.MinimumLevel}+")) { @@ -475,8 +477,8 @@ internal class ConsoleWindow : Window, IDisposable if (ImGui.Selectable(value.ToString(), value == EntryPoint.LogLevelSwitch.MinimumLevel)) { EntryPoint.LogLevelSwitch.MinimumLevel = value; - configuration.LogLevel = value; - configuration.QueueSave(); + this.configuration.LogLevel = value; + this.configuration.QueueSave(); this.QueueRefilter(); } } @@ -489,13 +491,13 @@ internal class ConsoleWindow : Window, IDisposable var settingsPopup = ImGui.BeginPopup("##console_settings"); if (settingsPopup) { - this.DrawSettingsPopup(configuration); + this.DrawSettingsPopup(); ImGui.EndPopup(); } else if (this.settingsPopupWasOpen) { // Prevent side effects in case Apply wasn't clicked - this.logLinesLimit = configuration.LogLinesLimit; + this.logLinesLimit = this.configuration.LogLinesLimit; } this.settingsPopupWasOpen = settingsPopup; @@ -643,18 +645,18 @@ internal class ConsoleWindow : Window, IDisposable } } - private void DrawSettingsPopup(DalamudConfiguration configuration) + private void DrawSettingsPopup() { if (ImGui.Checkbox("Open at startup", ref this.autoOpen)) { - configuration.LogOpenAtStartup = this.autoOpen; - configuration.QueueSave(); + this.configuration.LogOpenAtStartup = this.autoOpen; + this.configuration.QueueSave(); } if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) { - configuration.LogAutoScroll = this.autoScroll; - configuration.QueueSave(); + this.configuration.LogAutoScroll = this.autoScroll; + this.configuration.QueueSave(); } ImGui.TextUnformatted("Logs buffer"); @@ -663,8 +665,8 @@ internal class ConsoleWindow : Window, IDisposable { this.logLinesLimit = Math.Max(LogLinesMinimum, this.logLinesLimit); - configuration.LogLinesLimit = this.logLinesLimit; - configuration.QueueSave(); + this.configuration.LogLinesLimit = this.logLinesLimit; + this.configuration.QueueSave(); ImGui.CloseCurrentPopup(); } @@ -800,23 +802,18 @@ internal class ConsoleWindow : Window, IDisposable { try { - this.historyPos = -1; - for (var i = this.history.Count - 1; i >= 0; i--) - { - if (this.history[i] == this.commandText) - { - this.history.RemoveAt(i); - break; - } - } - - this.history.Add(this.commandText); - - if (this.commandText is "clear" or "cls") - { - this.QueueClear(); + if (string.IsNullOrEmpty(this.commandText)) return; - } + + this.historyPos = -1; + + if (this.commandText != this.configuration.LogCommandHistory.LastOrDefault()) + this.configuration.LogCommandHistory.Add(this.commandText); + + if (this.configuration.LogCommandHistory.Count > HistorySize) + this.configuration.LogCommandHistory.RemoveAt(0); + + this.configuration.QueueSave(); this.lastCmdSuccess = Service.Get().ProcessCommand(this.commandText); this.commandText = string.Empty; @@ -902,7 +899,7 @@ internal class ConsoleWindow : Window, IDisposable if (ptr.EventKey == ImGuiKey.UpArrow) { if (this.historyPos == -1) - this.historyPos = this.history.Count - 1; + this.historyPos = this.configuration.LogCommandHistory.Count - 1; else if (this.historyPos > 0) this.historyPos--; } @@ -910,7 +907,7 @@ internal class ConsoleWindow : Window, IDisposable { if (this.historyPos != -1) { - if (++this.historyPos >= this.history.Count) + if (++this.historyPos >= this.configuration.LogCommandHistory.Count) { this.historyPos = -1; } @@ -919,7 +916,7 @@ internal class ConsoleWindow : Window, IDisposable if (prevPos != this.historyPos) { - var historyStr = this.historyPos >= 0 ? this.history[this.historyPos] : string.Empty; + var historyStr = this.historyPos >= 0 ? this.configuration.LogCommandHistory[this.historyPos] : string.Empty; ptr.DeleteChars(0, ptr.BufTextLen); ptr.InsertChars(0, historyStr); From d0cba3eca8150f3c92d65861b7ca01f49e2ac9f5 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:01:26 +0200 Subject: [PATCH 04/23] build: 9.1.0.11 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 5cf16101f..b47accc9b 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.1.0.10 + 9.1.0.11 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 9fdc7048343e85b67f8efa65358de97390383e7d Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 10 Jun 2024 21:23:25 +0200 Subject: [PATCH 05/23] add missing history property to config --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 67c220800..0267042ed 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -228,6 +228,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public int LogLinesLimit { get; set; } = 10000; + /// + /// Gets or sets a list of commands that have been run in the console window. + /// + public List LogCommandHistory { get; set; } = new(); + /// /// Gets or sets a value indicating whether or not the dev bar should open at startup. /// From 6adf0d7bd9fd4ed8b4a5acb237a544034a8eb0e9 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 10 Jun 2024 21:24:25 +0200 Subject: [PATCH 06/23] build: 9.1.0.12 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index b47accc9b..b25aeecce 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.1.0.11 + 9.1.0.12 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From a12edcbae05966d4858ca970d4135b2827ca121a Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 11 Jun 2024 18:47:40 +0200 Subject: [PATCH 07/23] console: properly get private service --- Dalamud/Console/ConsoleManagerPluginScoped.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs index fed614cfc..755dd23e0 100644 --- a/Dalamud/Console/ConsoleManagerPluginScoped.cs +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -22,7 +22,8 @@ namespace Dalamud.Console; #pragma warning restore SA1015 public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService { - private readonly ConsoleManager console; + [ServiceManager.ServiceDependency] + private readonly ConsoleManager console = Service.Get(); private readonly List trackedEntries = new(); @@ -32,10 +33,8 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService /// The plugin this service belongs to. /// The console manager. [ServiceManager.ServiceConstructor] - internal ConsoleManagerPluginScoped(LocalPlugin plugin, ConsoleManager console) + internal ConsoleManagerPluginScoped(LocalPlugin plugin) { - this.console = console; - this.Prefix = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(plugin.InternalName); } From bc0bea03e0ee02f9670f27d3f7bb7267272e36b6 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 12 Jun 2024 00:01:50 +0200 Subject: [PATCH 08/23] add ICondition.Only() to check for a set of flags --- .../Game/ClientState/Conditions/Condition.cs | 19 +++++++++++++++++++ Dalamud/Plugin/Services/ICondition.cs | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index d281d7aec..23778288e 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -1,3 +1,5 @@ +using System.Linq; + using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -98,6 +100,20 @@ internal sealed class Condition : IInternalDisposableService, ICondition return false; } + /// + public bool Only(params ConditionFlag[] flags) + { + for (var i = 0; i < MaxConditionEntries; i++) + { + if (this[i] && flags.All(f => (int)f != i)) + { + return false; + } + } + + return true; + } + private void Dispose(bool disposing) { if (this.isDisposed) @@ -181,6 +197,9 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition /// public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags); + + /// + public bool Only(params ConditionFlag[] flags) => this.conditionService.Only(flags); private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value); } diff --git a/Dalamud/Plugin/Services/ICondition.cs b/Dalamud/Plugin/Services/ICondition.cs index 9700cef5a..3b74c333c 100644 --- a/Dalamud/Plugin/Services/ICondition.cs +++ b/Dalamud/Plugin/Services/ICondition.cs @@ -51,4 +51,12 @@ public interface ICondition /// Whether any single provided flag is set. /// The condition flags to check. public bool Any(params ConditionFlag[] flags); + + /// + /// Check if none but the provided condition flags are set. + /// This is not an exclusive check, it will return true if the provided flags are the only ones set. + /// + /// The condition flags to check for. + /// Whether only flags passed in are set. + public bool Only(params ConditionFlag[] flags); } From 6247542169005ff364c5ecd49c48ce6e2d344b4b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 14 Jun 2024 23:11:45 +0900 Subject: [PATCH 09/23] Skip IMM ImGui handling if ImGui is not initialized (#1836) --- Dalamud/Game/Gui/GameGui.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index d48700af7..858a825bc 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -496,6 +496,9 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui private char HandleImmDetour(IntPtr framework, char a2, byte a3) { var result = this.handleImmHook.Original(framework, a2, a3); + if (!ImGuiHelpers.IsImGuiInitialized) + return result; + return ImGui.GetIO().WantTextInput ? (char)0 : result; From c926a13848985bcfee55670217180b6aa7388241 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 00:53:25 +0200 Subject: [PATCH 10/23] tweak notification ux * Swap the "up" and "down" arrows to make more sense * Toggle collapse state when clicking, instead of dismissing --- .../ImGuiNotification/Internal/ActiveNotification.ImGui.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index fdccaab7b..16d58bea5 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -124,7 +124,7 @@ internal sealed partial class ActiveNotification if (this.Click is null) { if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - this.DismissNow(NotificationDismissReason.Manual); + this.Minimized = !this.Minimized; } else { @@ -277,12 +277,12 @@ internal sealed partial class ActiveNotification if (this.underlyingNotification.Minimized) { - if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height, drawActionButtons)) + if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height, drawActionButtons)) this.Minimized = false; } else { - if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height, drawActionButtons)) + if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height, drawActionButtons)) this.Minimized = true; } From 8d18940108068622b0a624e9fd19a369d8a054d8 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 01:00:50 +0200 Subject: [PATCH 11/23] initial implementation of new auto-update UX --- .../Internal/AutoUpdatePreference.cs | 42 ++ .../Internal/DalamudConfiguration.cs | 29 +- Dalamud/Console/ConsoleManagerPluginScoped.cs | 1 - Dalamud/Game/ChatHandlers.cs | 136 ------ Dalamud/Interface/DalamudWindowOpenKinds.cs | 5 + .../Interface/Internal/DalamudInterface.cs | 9 + .../DesignSystem/DalamudComponents.Buttons.cs | 56 +++ .../DalamudComponents.PluginPicker.cs | 79 ++++ .../DesignSystem/DalamudComponents.cs | 8 + .../Internal/Windows/ChangelogWindow.cs | 141 +++++- .../PluginInstaller/ProfileManagerWidget.cs | 45 +- .../Windows/Settings/SettingsWindow.cs | 57 ++- .../Settings/Tabs/SettingsTabAutoUpdate.cs | 254 ++++++++++ .../Settings/Tabs/SettingsTabGeneral.cs | 10 +- Dalamud/Plugin/DalamudPluginInterface.cs | 6 +- .../Internal/AutoUpdate/AutoUpdateBehavior.cs | 27 ++ .../Internal/AutoUpdate/AutoUpdateManager.cs | 439 ++++++++++++++++++ 17 files changed, 1115 insertions(+), 229 deletions(-) create mode 100644 Dalamud/Configuration/Internal/AutoUpdatePreference.cs create mode 100644 Dalamud/Interface/Internal/DesignSystem/DalamudComponents.Buttons.cs create mode 100644 Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs create mode 100644 Dalamud/Interface/Internal/DesignSystem/DalamudComponents.cs create mode 100644 Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs create mode 100644 Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs create mode 100644 Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs diff --git a/Dalamud/Configuration/Internal/AutoUpdatePreference.cs b/Dalamud/Configuration/Internal/AutoUpdatePreference.cs new file mode 100644 index 000000000..2b7dced11 --- /dev/null +++ b/Dalamud/Configuration/Internal/AutoUpdatePreference.cs @@ -0,0 +1,42 @@ +namespace Dalamud.Configuration.Internal; + +/// +/// Class representing a plugin that has opted in to auto-updating. +/// +internal class AutoUpdatePreference +{ + /// + /// Initializes a new instance of the class. + /// + /// The unique ID representing the plugin. + public AutoUpdatePreference(Guid pluginId) + { + this.WorkingPluginId = pluginId; + } + + /// + /// The kind of opt-in. + /// + public enum OptKind + { + /// + /// Never auto-update this plugin. + /// + NeverUpdate, + + /// + /// Always auto-update this plugin, regardless of the user's settings. + /// + AlwaysUpdate, + } + + /// + /// Gets or sets the unique ID representing the plugin. + /// + public Guid WorkingPluginId { get; set; } + + /// + /// Gets or sets the type of opt-in. + /// + public OptKind Kind { get; set; } = OptKind.AlwaysUpdate; +} diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 0267042ed..e5348d999 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -8,9 +8,9 @@ using System.Runtime.InteropServices; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.FontIdentifier; -using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Style; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Storage; using Dalamud.Utility; @@ -196,6 +196,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// /// Gets or sets a value indicating whether or not plugins should be auto-updated. /// + [Obsolete("Use AutoUpdateBehavior instead.")] public bool AutoUpdatePlugins { get; set; } /// @@ -229,7 +230,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public int LogLinesLimit { get; set; } = 10000; /// - /// Gets or sets a list of commands that have been run in the console window. + /// Gets or sets a list representing the command history for the Dalamud Console. /// public List LogCommandHistory { get; set; } = new(); @@ -434,7 +435,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// /// Gets or sets a list of plugins that testing builds should be downloaded for. /// - public List? PluginTestingOptIns { get; set; } + public List PluginTestingOptIns { get; set; } = []; + + /// + /// Gets or sets a list of plugins that have opted into or out of auto-updating. + /// + public List PluginAutoUpdatePreferences { get; set; } = []; /// /// Gets or sets a value indicating whether the FFXIV window should be toggled to immersive mode. @@ -466,6 +472,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public PluginInstallerOpenKind PluginInstallerOpen { get; set; } = PluginInstallerOpenKind.AllPlugins; + /// + /// Gets or sets a value indicating how auto-updating should behave. + /// + public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null; + + /// + /// Gets or sets a value indicating whether or not users should be notified regularly about pending updates. + /// + public bool CheckPeriodicallyForUpdates { get; set; } = true; + /// /// Load a configuration from the provided path. /// @@ -551,6 +567,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService private void SetDefaults() { +#pragma warning disable CS0618 // "Reduced motion" if (!this.ReduceMotions.HasValue) { @@ -572,6 +589,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService 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() diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs index 755dd23e0..b338ae0d8 100644 --- a/Dalamud/Console/ConsoleManagerPluginScoped.cs +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -31,7 +31,6 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService /// Initializes a new instance of the class. /// /// The plugin this service belongs to. - /// The console manager. [ServiceManager.ServiceConstructor] internal ConsoleManagerPluginScoped(LocalPlugin plugin) { diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 7f93ce6c2..09d825552 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -30,40 +30,6 @@ namespace Dalamud.Game; [ServiceManager.EarlyLoadedService] internal class ChatHandlers : IServiceType { - // private static readonly Dictionary UnicodeToDiscordEmojiDict = new() - // { - // { "", "<:ffxive071:585847382210642069>" }, - // { "", "<:ffxive083:585848592699490329>" }, - // }; - - // private readonly Dictionary handledChatTypeColors = new() - // { - // { XivChatType.CrossParty, Color.DodgerBlue }, - // { XivChatType.Party, Color.DodgerBlue }, - // { XivChatType.FreeCompany, Color.DeepSkyBlue }, - // { XivChatType.CrossLinkShell1, Color.ForestGreen }, - // { XivChatType.CrossLinkShell2, Color.ForestGreen }, - // { XivChatType.CrossLinkShell3, Color.ForestGreen }, - // { XivChatType.CrossLinkShell4, Color.ForestGreen }, - // { XivChatType.CrossLinkShell5, Color.ForestGreen }, - // { XivChatType.CrossLinkShell6, Color.ForestGreen }, - // { XivChatType.CrossLinkShell7, Color.ForestGreen }, - // { XivChatType.CrossLinkShell8, Color.ForestGreen }, - // { XivChatType.Ls1, Color.ForestGreen }, - // { XivChatType.Ls2, Color.ForestGreen }, - // { XivChatType.Ls3, Color.ForestGreen }, - // { XivChatType.Ls4, Color.ForestGreen }, - // { XivChatType.Ls5, Color.ForestGreen }, - // { XivChatType.Ls6, Color.ForestGreen }, - // { XivChatType.Ls7, Color.ForestGreen }, - // { XivChatType.Ls8, Color.ForestGreen }, - // { XivChatType.TellIncoming, Color.HotPink }, - // { XivChatType.PvPTeam, Color.SandyBrown }, - // { XivChatType.Urgent, Color.DarkViolet }, - // { XivChatType.NoviceNetwork, Color.SaddleBrown }, - // { XivChatType.Echo, Color.Gray }, - // }; - private static readonly ModuleLog Log = new("CHATHANDLER"); private readonly Regex rmtRegex = new( @@ -106,8 +72,6 @@ internal class ChatHandlers : IServiceType private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled); - private readonly DalamudLinkPayload openInstallerWindowLink; - [ServiceManager.ServiceDependency] private readonly Dalamud dalamud = Service.Get(); @@ -115,19 +79,12 @@ internal class ChatHandlers : IServiceType private readonly DalamudConfiguration configuration = Service.Get(); private bool hasSeenLoadingMsg; - private bool startedAutoUpdatingPlugins; - private CancellationTokenSource deferredAutoUpdateCts = new(); [ServiceManager.ServiceConstructor] private ChatHandlers(ChatGui chatGui) { chatGui.CheckMessageHandled += this.OnCheckMessageHandled; chatGui.ChatMessage += this.OnChatMessage; - - this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) => - { - Service.GetNullable()?.OpenPluginInstallerTo(PluginInstallerOpenKind.InstalledPlugins); - }); } /// @@ -135,11 +92,6 @@ internal class ChatHandlers : IServiceType /// public string? LastLink { get; private set; } - /// - /// Gets a value indicating whether or not auto-updates have already completed this session. - /// - public bool IsAutoUpdateComplete { get; private set; } - private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) { var textVal = message.TextValue; @@ -176,9 +128,6 @@ internal class ChatHandlers : IServiceType { if (!this.hasSeenLoadingMsg) this.PrintWelcomeMessage(); - - if (!this.startedAutoUpdatingPlugins) - this.AutoUpdatePluginsWithRetry(); } // For injections while logged in @@ -273,89 +222,4 @@ internal class ChatHandlers : IServiceType this.hasSeenLoadingMsg = true; } - - private void AutoUpdatePluginsWithRetry() - { - var firstAttempt = this.AutoUpdatePlugins(); - if (!firstAttempt) - { - Task.Run(() => - { - Task.Delay(30_000, this.deferredAutoUpdateCts.Token); - this.AutoUpdatePlugins(); - }); - } - } - - private bool AutoUpdatePlugins() - { - var chatGui = Service.GetNullable(); - var pluginManager = Service.GetNullable(); - var notifications = Service.GetNullable(); - var condition = Service.GetNullable(); - - if (chatGui == null || pluginManager == null || notifications == null || condition == null) - { - Log.Warning("Aborting auto-update because a required service was not loaded."); - return false; - } - - if (condition.Any(ConditionFlag.BoundByDuty, ConditionFlag.BoundByDuty56, ConditionFlag.BoundByDuty95)) - { - Log.Warning("Aborting auto-update because the player is in a duty."); - return false; - } - - if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any()) - { - // Plugins aren't ready yet. - // TODO: We should retry. This sucks, because it means we won't ever get here again until another notice. - Log.Warning("Aborting auto-update because plugins weren't loaded or ready."); - return false; - } - - this.startedAutoUpdatingPlugins = true; - - Log.Debug("Beginning plugin auto-update process..."); - Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task => - { - this.IsAutoUpdateComplete = true; - - if (task.IsFaulted) - { - Log.Error(task.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates.")); - return; - } - - var updatedPlugins = task.Result.ToList(); - if (updatedPlugins.Any()) - { - if (this.configuration.AutoUpdatePlugins) - { - Service.Get().PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:")); - notifications.AddNotification(Loc.Localize("NotificationUpdatedPlugins", "{0} of your plugins were updated.").Format(updatedPlugins.Count), Loc.Localize("NotificationAutoUpdate", "Auto-Update"), NotificationType.Info); - } - else - { - chatGui.Print(new XivChatEntry - { - Message = new SeString(new List() - { - new TextPayload(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")), - new TextPayload(" ["), - new UIForegroundPayload(500), - this.openInstallerWindowLink, - new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")), - RawPayload.LinkTerminator, - new UIForegroundPayload(0), - new TextPayload("]"), - }), - Type = XivChatType.Urgent, - }); - } - } - }); - - return true; - } } diff --git a/Dalamud/Interface/DalamudWindowOpenKinds.cs b/Dalamud/Interface/DalamudWindowOpenKinds.cs index 18ecf5386..1f82cca49 100644 --- a/Dalamud/Interface/DalamudWindowOpenKinds.cs +++ b/Dalamud/Interface/DalamudWindowOpenKinds.cs @@ -35,6 +35,11 @@ public enum SettingsOpenKind /// Open to the "Look & Feel" page. /// LookAndFeel, + + /// + /// Open to the "Auto Updates" page. + /// + AutoUpdates, /// /// Open to the "Server Info Bar" page. diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 2eb8299b3..f57355112 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -211,6 +211,15 @@ internal class DalamudInterface : IInternalDisposableService set => this.isImGuiDrawDevMenu = value; } + /// + /// Gets or sets a value indicating whether the plugin installer is open. + /// + public bool IsPluginInstallerOpen + { + get => this.pluginWindow.IsOpen; + set => this.pluginWindow.IsOpen = value; + } + /// void IInternalDisposableService.DisposeService() { diff --git a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.Buttons.cs b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.Buttons.cs new file mode 100644 index 000000000..d525af484 --- /dev/null +++ b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.Buttons.cs @@ -0,0 +1,56 @@ +using System.Numerics; + +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.DesignSystem; + +/// +/// Private ImGui widgets for use inside Dalamud. +/// +internal static partial class DalamudComponents +{ + private static readonly Vector2 ButtonPadding = new(8 * ImGuiHelpers.GlobalScale, 6 * ImGuiHelpers.GlobalScale); + private static readonly Vector4 SecondaryButtonBackground = new(0, 0, 0, 0); + + private static Vector4 PrimaryButtonBackground => ImGuiColors.TankBlue; + + /// + /// Draw a "primary style" button. + /// + /// The text to show. + /// True if the button was clicked. + internal static bool PrimaryButton(string text) + { + using (ImRaii.PushColor(ImGuiCol.Button, PrimaryButtonBackground)) + { + return Button(text); + } + } + + /// + /// Draw a "secondary style" button. + /// + /// The text to show. + /// True if the button was clicked. + internal static bool SecondaryButton(string text) + { + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1)) + using (var buttonColor = ImRaii.PushColor(ImGuiCol.Button, SecondaryButtonBackground)) + { + buttonColor.Push(ImGuiCol.Border, ImGuiColors.DalamudGrey3); + return Button(text); + } + } + + private static bool Button(string text) + { + using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, ButtonPadding)) + { + return ImGui.Button(text); + } + } +} diff --git a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs new file mode 100644 index 000000000..f0ce6bc82 --- /dev/null +++ b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs @@ -0,0 +1,79 @@ +using System.Linq; +using System.Numerics; + +using CheapLoc; + +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.DesignSystem; + +/// +/// Private ImGui widgets for use inside Dalamud. +/// +internal static partial class DalamudComponents +{ + /// + /// Draw a "picker" popup to chose a plugin. + /// + /// The ID of the popup. + /// String holding the search input. + /// Action to be called if a plugin is clicked. + /// Function that should return true if a plugin should show as disabled. + /// Function that should return true if a plugin should not appear in the list. + /// An ImGuiID to open the popup. + internal static uint DrawPluginPicker(string id, ref string pickerSearch, Action onClicked, Func pluginDisabled, Func? pluginFiltered = null) + { + 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))) + { + // TODO: Plugin searching should be abstracted... installer and this should use the same search + var plugins = pm.InstalledPlugins.Where( + x => x.Manifest.SupportsProfiles && + (currentSearchString.IsNullOrWhitespace() || x.Manifest.Name.Contains( + currentSearchString, + StringComparison.InvariantCultureIgnoreCase))) + .Where(pluginFiltered ?? (_ => true)); + + foreach (var plugin in plugins) + { + 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(); + } + } + + return addPluginToProfilePopupId; + } + + private static partial class Locs + { + public static string SearchHint => Loc.Localize("ProfileManagerSearchHint", "Search..."); + } +} diff --git a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.cs b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.cs new file mode 100644 index 000000000..be3c90640 --- /dev/null +++ b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.cs @@ -0,0 +1,8 @@ +namespace Dalamud.Interface.Internal.DesignSystem; + +/// +/// Private ImGui widgets for use inside Dalamud. +/// +internal static partial class DalamudComponents +{ +} diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index ae59db36a..613fc7d28 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,6 +1,8 @@ using System.Linq; using System.Numerics; +using CheapLoc; + using Dalamud.Configuration.Internal; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; @@ -12,6 +14,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Storage.Assets; using Dalamud.Utility; using ImGuiNET; @@ -23,6 +26,8 @@ namespace Dalamud.Interface.Internal.Windows; /// internal sealed class ChangelogWindow : Window, IDisposable { + private const AutoUpdateBehavior DefaultAutoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo; + private const string WarrantsChangelogForMajorMinor = "9.0."; private const string ChangeLog = @@ -47,15 +52,32 @@ internal sealed class ChangelogWindow : Window, IDisposable Point2 = new Vector2(2f), }; - private readonly InOutCubic bodyFade = new(TimeSpan.FromSeconds(1f)) + private readonly InOutCubic bodyFade = new(TimeSpan.FromSeconds(1.3f)) { Point1 = Vector2.Zero, Point2 = Vector2.One, }; + private readonly InOutCubic titleFade = new(TimeSpan.FromSeconds(1f)) + { + Point1 = Vector2.Zero, + Point2 = Vector2.One, + }; + + private readonly InOutCubic fadeOut = new(TimeSpan.FromSeconds(0.8f)) + { + Point1 = Vector2.One, + Point2 = Vector2.Zero, + }; + private State state = State.WindowFadeIn; - + private bool needFadeRestart = false; + + private bool isFadingOutForStateChange = false; + private State? stateAfterFadeOut; + + private AutoUpdateBehavior autoUpdateBehavior = DefaultAutoUpdateBehavior; /// /// Initializes a new instance of the class. @@ -90,6 +112,7 @@ internal sealed class ChangelogWindow : Window, IDisposable WindowFadeIn, ExplainerIntro, ExplainerApiBump, + AskAutoUpdate, Links, } @@ -114,12 +137,19 @@ internal sealed class ChangelogWindow : Window, IDisposable this.tsmWindow.AllowDrawing = false; _ = this.bannerFont; + + this.isFadingOutForStateChange = false; + this.stateAfterFadeOut = null; this.state = State.WindowFadeIn; this.windowFade.Reset(); this.bodyFade.Reset(); + this.titleFade.Reset(); + this.fadeOut.Reset(); this.needFadeRestart = true; + this.autoUpdateBehavior = DefaultAutoUpdateBehavior; + base.OnOpen(); } @@ -130,6 +160,10 @@ internal sealed class ChangelogWindow : Window, IDisposable this.tsmWindow.AllowDrawing = true; Service.Get().SetCreditsDarkeningAnimation(false); + + var configuration = Service.Get(); + configuration.AutoUpdateBehavior = this.autoUpdateBehavior; + configuration.QueueSave(); } /// @@ -144,10 +178,13 @@ internal sealed class ChangelogWindow : Window, IDisposable if (this.needFadeRestart) { this.windowFade.Restart(); + this.titleFade.Restart(); this.needFadeRestart = false; } this.windowFade.Update(); + this.titleFade.Update(); + this.fadeOut.Update(); ImGui.SetNextWindowBgAlpha(Math.Clamp(this.windowFade.EasedPoint.X, 0, 0.9f)); this.Size = new Vector2(900, 400); @@ -207,8 +244,9 @@ internal sealed class ChangelogWindow : Window, IDisposable return; ImGuiHelpers.ScaledDummy(20); - - using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) + + var titleFadeVal = this.isFadingOutForStateChange ? this.fadeOut.EasedPoint.X : this.titleFade.EasedPoint.X; + using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(titleFadeVal, 0f, 1f))) { using var font = this.bannerFont.Value.Push(); @@ -223,6 +261,10 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGuiHelpers.CenteredText("Plugin Updates"); break; + case State.AskAutoUpdate: + ImGuiHelpers.CenteredText("Auto-Updates"); + break; + case State.Links: ImGuiHelpers.CenteredText("Enjoy!"); break; @@ -236,10 +278,30 @@ internal sealed class ChangelogWindow : Window, IDisposable this.state = State.ExplainerIntro; this.bodyFade.Restart(); } + + if (this.isFadingOutForStateChange && this.fadeOut.IsDone) + { + this.state = this.stateAfterFadeOut ?? throw new Exception("State after fade out is null"); + + this.bodyFade.Restart(); + this.titleFade.Restart(); + + this.isFadingOutForStateChange = false; + this.stateAfterFadeOut = null; + } this.bodyFade.Update(); - using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.bodyFade.EasedPoint.X, 0, 1f))) + var bodyFadeVal = this.isFadingOutForStateChange ? this.fadeOut.EasedPoint.X : this.bodyFade.EasedPoint.X; + using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(bodyFadeVal, 0, 1f))) { + void GoToNextState(State nextState) + { + this.isFadingOutForStateChange = true; + this.stateAfterFadeOut = nextState; + + this.fadeOut.Restart(); + } + void DrawNextButton(State nextState) { // Draw big, centered next button at the bottom of the window @@ -249,10 +311,9 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.SetCursorPosY(windowSize.Y - buttonHeight - (20 * ImGuiHelpers.GlobalScale)); ImGuiHelpers.CenterCursorFor((int)buttonWidth); - if (ImGui.Button(buttonText, new Vector2(buttonWidth, buttonHeight))) + if (ImGui.Button(buttonText, new Vector2(buttonWidth, buttonHeight)) && !this.isFadingOutForStateChange) { - this.state = nextState; - this.bodyFade.Restart(); + GoToNextState(nextState); } } @@ -286,7 +347,65 @@ internal sealed class ChangelogWindow : Window, IDisposable this.apiBumpExplainerTexture.Value.ImGuiHandle, this.apiBumpExplainerTexture.Value.Size); - DrawNextButton(State.Links); + DrawNextButton(State.AskAutoUpdate); + break; + + case State.AskAutoUpdate: + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateHint", + "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 never bother you about updates while you are not idle.")); + + ImGuiHelpers.ScaledDummy(15); + + /* + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior", + "When the game starts...")); + var behaviorInt = (int)this.autoUpdateBehavior; + ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNone", "Do not check for updates automatically"), ref behaviorInt, (int)AutoUpdateBehavior.None); + ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNotify", "Only notify me of new updates"), ref behaviorInt, (int)AutoUpdateBehavior.OnlyNotify); + ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateMainRepo", "Auto-update main repository plugins"), ref behaviorInt, (int)AutoUpdateBehavior.UpdateMainRepo); + this.autoUpdateBehavior = (AutoUpdateBehavior)behaviorInt; + */ + + bool DrawCenteredButton(string text, float height) + { + var buttonHeight = height * ImGuiHelpers.GlobalScale; + var buttonWidth = ImGui.CalcTextSize(text).X + 50 * ImGuiHelpers.GlobalScale; + ImGuiHelpers.CenterCursorFor((int)buttonWidth); + + return ImGui.Button(text, new Vector2(buttonWidth, buttonHeight)) && + !this.isFadingOutForStateChange; + } + + using (ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.DPSRed)) + { + if (DrawCenteredButton("Enable auto-updates", 30)) + { + this.autoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo; + GoToNextState(State.Links); + } + } + + ImGuiHelpers.ScaledDummy(2); + + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1)) + using (var buttonColor = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero)) + { + buttonColor.Push(ImGuiCol.Border, ImGuiColors.DalamudGrey3); + if (DrawCenteredButton("Disable auto-updates", 25)) + { + this.autoUpdateBehavior = AutoUpdateBehavior.OnlyNotify; + GoToNextState(State.Links); + } + } + break; case State.Links: @@ -356,12 +475,12 @@ internal sealed class ChangelogWindow : Window, IDisposable // Draw close button in the top right corner ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 100f); var btnAlpha = Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f); - ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.DalamudRed.WithAlpha(btnAlpha).Desaturate(0.3f)); + ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.DPSRed.WithAlpha(btnAlpha).Desaturate(0.3f)); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudWhite.WithAlpha(btnAlpha)); var childSize = ImGui.GetWindowSize(); var closeButtonSize = 15 * ImGuiHelpers.GlobalScale; - ImGui.SetCursorPos(new Vector2(childSize.X - closeButtonSize - 5, 10 * ImGuiHelpers.GlobalScale)); + ImGui.SetCursorPos(new Vector2(childSize.X - closeButtonSize - (10 * ImGuiHelpers.GlobalScale), 10 * ImGuiHelpers.GlobalScale)); if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { Dismiss(); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index d0dc01ce5..379485517 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -7,12 +7,12 @@ using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal.DesignSystem; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Profiles; -using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; using Serilog; @@ -300,39 +300,16 @@ internal class ProfileManagerWidget return; } - const string addPluginToProfilePopup = "###addPluginToProfile"; - var addPluginToProfilePopupId = ImGui.GetID(addPluginToProfilePopup); - using (var popup = ImRaii.Popup(addPluginToProfilePopup)) - { - if (popup.Success) + var addPluginToProfilePopupId = DalamudComponents.DrawPluginPicker( + "###addPluginToProfilePicker", + ref this.pickerSearch, + plugin => { - var width = ImGuiHelpers.GlobalScale * 300; - - using var disabled = ImRaii.Disabled(profman.IsBusy); - - ImGui.SetNextItemWidth(width); - ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref this.pickerSearch, 255); - - if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80))) - { - // TODO: Plugin searching should be abstracted... installer and this should use the same search - foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && - (this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant())))) - { - using var disabled2 = - ImRaii.Disabled(profile.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName)); - - if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}")) - { - Task.Run(() => profile.AddOrUpdateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, true, false)) - .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); - } - } - - ImGui.EndListBox(); - } - } - } + Task.Run(() => profile.AddOrUpdateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, true, false)) + .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); + }, + plugin => !plugin.Manifest.SupportsProfiles || + profile.Plugins.Any(x => x.WorkingPluginId == plugin.EffectiveWorkingPluginId)); var didAny = false; @@ -603,8 +580,6 @@ internal class ProfileManagerWidget public static string BackToOverview => Loc.Localize("ProfileManagerBackToOverview", "Back to overview"); - public static string SearchHint => Loc.Localize("ProfileManagerSearchHint", "Search..."); - public static string AddProfileHint => Loc.Localize("ProfileManagerAddProfileHint", "No collections! Add one!"); public static string CloneProfileHint => Loc.Localize("ProfileManagerCloneProfile", "Clone this collection"); diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index fd4949533..196a11ab1 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -21,7 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings; /// internal class SettingsWindow : Window { - private SettingsTab[]? tabs; + private readonly SettingsTab[] tabs; private string searchInput = string.Empty; private bool isSearchInputPrefilled = false; @@ -42,6 +42,16 @@ internal class SettingsWindow : Window }; this.SizeCondition = ImGuiCond.FirstUseEver; + + this.tabs = + [ + new SettingsTabGeneral(), + new SettingsTabLook(), + new SettingsTabAutoUpdates(), + new SettingsTabDtr(), + new SettingsTabExperimental(), + new SettingsTabAbout() + ]; } /// @@ -75,15 +85,6 @@ internal class SettingsWindow : Window /// public override void OnOpen() { - this.tabs ??= new SettingsTab[] - { - new SettingsTabGeneral(), - new SettingsTabLook(), - new SettingsTabDtr(), - new SettingsTabExperimental(), - new SettingsTabAbout(), - }; - foreach (var settingsTab in this.tabs) { settingsTab.Load(); @@ -142,7 +143,7 @@ internal class SettingsWindow : Window flags |= ImGuiTabItemFlags.SetSelected; this.setActiveTab = null; } - + using var tab = ImRaii.TabItem(settingsTab.Title, flags); if (tab) { @@ -152,10 +153,14 @@ internal class SettingsWindow : Window settingsTab.OnOpen(); } + // Don't add padding for the about tab(credits) + using var padding = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(2, 2), + settingsTab is not SettingsTabAbout); + using var borderColor = ImRaii.PushColor(ImGuiCol.Border, ImGui.GetColorU32(ImGuiCol.ChildBg)); using var tabChild = ImRaii.Child( $"###settings_scrolling_{settingsTab.Title}", new Vector2(-1, -1), - false); + true); if (tabChild) settingsTab.Draw(); } @@ -281,25 +286,15 @@ internal class SettingsWindow : Window private void SetOpenTab(SettingsOpenKind kind) { - switch (kind) + this.setActiveTab = kind switch { - case SettingsOpenKind.General: - this.setActiveTab = this.tabs[0]; - break; - case SettingsOpenKind.LookAndFeel: - this.setActiveTab = this.tabs[1]; - break; - case SettingsOpenKind.ServerInfoBar: - this.setActiveTab = this.tabs[2]; - break; - case SettingsOpenKind.Experimental: - this.setActiveTab = this.tabs[3]; - break; - case SettingsOpenKind.About: - this.setActiveTab = this.tabs[4]; - break; - default: - throw new ArgumentOutOfRangeException(nameof(kind), kind, null); - } + SettingsOpenKind.General => this.tabs[0], + SettingsOpenKind.LookAndFeel => this.tabs[1], + SettingsOpenKind.AutoUpdates => this.tabs[2], + SettingsOpenKind.ServerInfoBar => this.tabs[3], + SettingsOpenKind.Experimental => this.tabs[4], + SettingsOpenKind.About => this.tabs[5], + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), + }; } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs new file mode 100644 index 000000000..0b18e59d9 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -0,0 +1,254 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; + +using CheapLoc; +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.Internal.DesignSystem; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.AutoUpdate; +using Dalamud.Plugin.Internal.Types; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; + +[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] +public class SettingsTabAutoUpdates : SettingsTab +{ + private AutoUpdateBehavior behavior; + private bool checkPeriodically; + private string pickerSearch = string.Empty; + private List autoUpdatePreferences = []; + + public override SettingsEntry[] Entries { get; } = Array.Empty(); + + public override string Title => Loc.Localize("DalamudSettingsAutoUpdates", "Auto-Updates"); + + public override void Draw() + { + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateHint", + "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 never bother you about updates while you are not idle.")); + + ImGuiHelpers.ScaledDummy(8); + + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior", + "When the game starts...")); + var behaviorInt = (int)this.behavior; + ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNone", "Do not check for updates automatically"), ref behaviorInt, (int)AutoUpdateBehavior.None); + ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNotify", "Only notify me of new updates"), ref behaviorInt, (int)AutoUpdateBehavior.OnlyNotify); + ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateMainRepo", "Auto-update main repository plugins"), ref behaviorInt, (int)AutoUpdateBehavior.UpdateMainRepo); + ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateAll", "Auto-update all plugins"), ref behaviorInt, (int)AutoUpdateBehavior.UpdateAll); + this.behavior = (AutoUpdateBehavior)behaviorInt; + + if (this.behavior == AutoUpdateBehavior.UpdateAll) + { + var warning = Loc.Localize( + "DalamudSettingsAutoUpdateAllWarning", + "Warning: This will update all plugins, including those not from the main repository.\n" + + "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("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.")); + + if (this.autoUpdatePreferences.Count == 0) + { + ImGuiHelpers.ScaledDummy(20); + + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) + { + ImGuiHelpers.CenteredText(Loc.Localize("DalamudSettingsAutoUpdateOptedInHint2", + "You did not override auto-updates for any plugins yet.")); + } + + 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 + .FirstOrDefault(x => x.EffectiveWorkingPluginId == preference.WorkingPluginId); + + var btnOffset = 2; + + if (pmPlugin != null) + { + var cursorBeforeIcon = ImGui.GetCursorPos(); + pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon, out _); + icon ??= pic.DefaultIcon; + + ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight)); + + if (pmPlugin.IsDev) + { + ImGui.SetCursorPos(cursorBeforeIcon); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.7f); + ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); + ImGui.PopStyleVar(); + } + + ImGui.SameLine(); + + var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}"; + var textHeight = ImGui.CalcTextSize(text); + var before = ImGui.GetCursorPos(); + + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); + ImGui.TextUnformatted(text); + + ImGui.SetCursorPos(before); + } + else + { + ImGui.Image(pic.DefaultIcon.ImGuiHandle, new Vector2(pluginLineHeight)); + ImGui.SameLine(); + + var text = Loc.Localize("DalamudSettingsAutoUpdateOptInUnknownPlugin", "Unknown plugin"); + var textHeight = ImGui.CalcTextSize(text); + var before = ImGui.GetCursorPos(); + + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); + ImGui.TextUnformatted(text); + + ImGui.SetCursorPos(before); + } + + ImGui.SameLine(); + ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 320)); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); + + string OptKindToString(AutoUpdatePreference.OptKind kind) + { + return kind switch + { + AutoUpdatePreference.OptKind.NeverUpdate => Loc.Localize("DalamudSettingsAutoUpdateOptInNeverUpdate", "Never update this"), + AutoUpdatePreference.OptKind.AlwaysUpdate => Loc.Localize("DalamudSettingsAutoUpdateOptInAlwaysUpdate", "Always update this"), + _ => throw new ArgumentOutOfRangeException(), + }; + } + + ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 250); + if (ImGui.BeginCombo( + $"###autoUpdateBehavior{preference.WorkingPluginId}", + OptKindToString(preference.Kind))) + { + foreach (var kind in Enum.GetValues()) + { + if (ImGui.Selectable(OptKindToString(kind))) + { + preference.Kind = kind; + } + } + + ImGui.EndCombo(); + } + + ImGui.SameLine(); + ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * btnOffset) - 5); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); + + if (ImGuiComponents.IconButton($"###removePlugin{preference.WorkingPluginId}", FontAwesomeIcon.Trash)) + { + wantRemovePluginGuid = preference.WorkingPluginId; + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(Loc.Localize("DalamudSettingsAutoUpdateOptInRemove", "Remove this override")); + } + + if (wantRemovePluginGuid != null) + { + this.autoUpdatePreferences.RemoveAll(x => x.WorkingPluginId == wantRemovePluginGuid); + } + } + + 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)); + if (ImGuiComponents.IconButtonWithText(addButtonIcon, addButtonText)) + { + this.pickerSearch = string.Empty; + ImGui.OpenPopup(pickerId); + } + + base.Draw(); + } + + public override void Load() + { + var configuration = Service.Get(); + + this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None; + this.checkPeriodically = configuration.CheckPeriodicallyForUpdates; + this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences; + + base.Load(); + } + + public override void Save() + { + var configuration = Service.Get(); + + configuration.AutoUpdateBehavior = this.behavior; + configuration.CheckPeriodicallyForUpdates = this.checkPeriodically; + configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences; + + base.Save(); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs index ea345e9cf..c96163835 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs @@ -3,6 +3,7 @@ using CheapLoc; using Dalamud.Game.Text; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Plugin.Internal.AutoUpdate; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -20,9 +21,8 @@ public class SettingsTabGeneral : SettingsTab Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."), c => c.GeneralChatType, (v, c) => c.GeneralChatType = v, - warning: (v) => + validity: (v) => { - // TODO: Maybe actually implement UI for the validity check... if (v == XivChatType.None) return Loc.Localize("DalamudSettingsChannelNone", "Do not pick \"None\"."); @@ -62,12 +62,6 @@ public class SettingsTabGeneral : SettingsTab c => c.PrintPluginsWelcomeMsg, (v, c) => c.PrintPluginsWelcomeMsg = v), - new SettingsEntry( - Loc.Localize("DalamudSettingsAutoUpdatePlugins", "Auto-update plugins"), - Loc.Localize("DalamudSettingsAutoUpdatePluginsMsgHint", "Automatically update plugins when logging in with a character."), - c => c.AutoUpdatePlugins, - (v, c) => c.AutoUpdatePlugins = v), - new SettingsEntry( Loc.Localize("DalamudSettingsSystemMenu", "Dalamud buttons in system menu"), Loc.Localize("DalamudSettingsSystemMenuMsgHint", "Add buttons for Dalamud plugins and settings to the system menu."), diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index d53c620f4..cf56ee0fd 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -9,7 +9,6 @@ using System.Reflection; using Dalamud.Configuration; using Dalamud.Configuration.Internal; using Dalamud.Data; -using Dalamud.Game; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.Sanitizer; @@ -20,6 +19,7 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; @@ -27,8 +27,6 @@ using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; using Dalamud.Utility; -using static Dalamud.Interface.Internal.Windows.PluginInstaller.PluginInstallerWindow; - namespace Dalamud.Plugin; /// @@ -114,7 +112,7 @@ public sealed class DalamudPluginInterface : IDisposable /// /// Gets a value indicating whether or not auto-updates have already completed this session. /// - public bool IsAutoUpdateComplete => Service.Get().IsAutoUpdateComplete; + public bool IsAutoUpdateComplete => Service.Get().IsAutoUpdateComplete; /// /// Gets the repository from which this plugin was installed. diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs new file mode 100644 index 000000000..2d5dec970 --- /dev/null +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Plugin.Internal.AutoUpdate; + +/// +/// Enum describing how plugins should be auto-updated at startup-. +/// +internal enum AutoUpdateBehavior +{ + /// + /// Plugins should not be updated and the user should not be notified. + /// + None, + + /// + /// The user should merely be notified about updates. + /// + OnlyNotify, + + /// + /// Only plugins from the main repository should be updated. + /// + UpdateMainRepo, + + /// + /// All plugins should be updated. + /// + UpdateAll, +} diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs new file mode 100644 index 000000000..3e12ef600 --- /dev/null +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -0,0 +1,439 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Dalamud.Configuration.Internal; +using Dalamud.Console; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.DesignSystem; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +using ImGuiNET; + +namespace Dalamud.Plugin.Internal.AutoUpdate; + +// TODO: Loc + +/// +/// Class to manage automatic updates for plugins. +/// +[ServiceManager.EarlyLoadedService] +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(1.5); + + /// + /// Time we should wait after unblocking to nag the user. + /// Used to prevent spamming a nag, for example, right after an user leaves a duty. + /// + private static readonly TimeSpan CooldownAfterUnblock = TimeSpan.FromSeconds(30); + + [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? lastUpdateCheckTime; + private DateTime? unblockedSince; + + private bool hasStartedInitialUpdateThisSession; + + private IActiveNotification? updateNotification; + + private Task? autoUpdateTask; + + /// + /// Initializes a new instance of the class. + /// + /// Console service. + [ServiceManager.ServiceConstructor] + public AutoUpdateManager(ConsoleManager console) + { + Service.GetAsync().ContinueWith( + t => + { + t.Result.Login += this.OnLogin; + t.Result.Logout += this.OnLogout; + }); + Service.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; }); + + this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", true); + console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () => + { + this.hasStartedInitialUpdateThisSession = false; + this.OnLogin(); + return true; + }); + console.AddCommand("dalamud.autoupdate.force_check", "Force a check for updates", () => + { + this.lastUpdateCheckTime = DateTime.Now - TimeBetweenUpdateChecks; + 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; } + + 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, + + AutoUpdateBehavior.UpdateMainRepo => UpdateListingRestriction.AllowMainRepo, + AutoUpdateBehavior.UpdateAll => UpdateListingRestriction.Unrestricted, + _ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null), + }; + } + + private void OnUpdate(IFramework framework) + { + if (this.loginTime == null) + return; + + var autoUpdateTaskInProgress = this.autoUpdateTask is not null && !this.autoUpdateTask.IsCompleted; + var isUnblocked = this.CanUpdateOrNag() && !autoUpdateTaskInProgress; + + if (this.unblockedSince == null && isUnblocked) + { + this.unblockedSince = DateTime.Now; + } + else if (this.unblockedSince != null && !isUnblocked) + { + this.unblockedSince = null; + + // Remove all notifications if we're not actively updating. The user probably doesn't care now. + if (this.updateNotification != null && !autoUpdateTaskInProgress) + { + this.updateNotification.DismissNow(); + this.updateNotification = null; + } + } + + // If we're blocked, we don't do anything. + if (!isUnblocked) + return; + + var isInUnblockedCooldown = + this.unblockedSince != null && DateTime.Now - this.unblockedSince < CooldownAfterUnblock; + + // If we're in the unblock cooldown period, we don't nag the user. This is intended to prevent us + // from showing update notifications right after the user leaves a duty, for example. + if (isInUnblockedCooldown && this.hasStartedInitialUpdateThisSession) + return; + + var behavior = this.config.AutoUpdateBehavior ?? AutoUpdateBehavior.None; + + // 1. This is the initial update after login. We only run this exactly once and this is + // the only time we actually install updates automatically. + if (!this.hasStartedInitialUpdateThisSession && DateTime.Now > this.loginTime.Value.Add(UpdateTimeAfterLogin)) + { + this.lastUpdateCheckTime = DateTime.Now; + this.hasStartedInitialUpdateThisSession = true; + + var currentlyUpdatablePlugins = this.GetAvailablePluginUpdates(DecideUpdateListingRestriction(behavior)); + + if (currentlyUpdatablePlugins.Count == 0) + { + this.IsAutoUpdateComplete = true; + 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) + { + // List all plugins in the notification + Log.Verbose("Ran initial update, notifying for {Num} plugins", currentlyUpdatablePlugins.Count); + this.NotifyUpdatesAreAvailable(currentlyUpdatablePlugins); + return; + } + + Log.Verbose("Ran initial update, updating {Num} plugins", currentlyUpdatablePlugins.Count); + this.KickOffAutoUpdates(currentlyUpdatablePlugins); + return; + } + + // 2. Continuously check for updates while the game is running. We run these every once in a while and + // will only show a notification here that lets people start the update or open the installer. + if (this.config.CheckPeriodicallyForUpdates && + this.lastUpdateCheckTime != null && + DateTime.Now - this.lastUpdateCheckTime > TimeBetweenUpdateChecks && + this.updateNotification == null) + { + this.pluginManager.ReloadPluginMastersAsync() + .ContinueWith( + t => + { + if (t.IsFaulted || t.IsCanceled) + { + Log.Error(t.Exception!, "Failed to reload plugin masters for auto-update"); + } + + this.NotifyUpdatesAreAvailable( + this.GetAvailablePluginUpdates( + DecideUpdateListingRestriction(behavior))); + }); + + this.lastUpdateCheckTime = DateTime.Now; + } + } + + 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; + + return this.updateNotification!; + } + + private void KickOffAutoUpdates(ICollection updatablePlugins) + { + this.autoUpdateTask = + Task.Run(() => this.RunAutoUpdates(updatablePlugins)) + .ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error(t.Exception!, "Failed to run auto-updates"); + } + else if (t.IsCanceled) + { + Log.Warning("Auto-update task was canceled"); + } + + this.autoUpdateTask = null; + this.IsAutoUpdateComplete = true; + }); + } + + private async Task RunAutoUpdates(ICollection updatablePlugins) + { + var pluginStates = new List(); + + Log.Information("Found {UpdatablePluginsCount} plugins to update", updatablePlugins.Count); + + if (updatablePlugins.Count == 0) + return; + + var notification = this.GetBaseNotification(new Notification + { + Title = "Updating plugins...", + Content = $"Preparing to update {updatablePlugins.Count} plugins...", + Type = NotificationType.Info, + InitialDuration = TimeSpan.MaxValue, + ShowIndeterminateIfNoExpiry = false, + UserDismissable = false, + Progress = 0, + Icon = INotificationIcon.From(FontAwesomeIcon.Download), + Minimized = false, + }); + + var numDone = 0; + // TODO: This is NOT correct, we need to do this inside PM to be able to avoid notifying for each of these and avoid + // refreshing the plugin list until we're done. See PluginManager::UpdatePluginsAsync(). + // Maybe have a function in PM that can take a list of AvailablePluginUpdate instead and update them all, + // and get rid of UpdatePluginsAsync()? Will have to change the installer a bit but that might be for the better API-wise. + foreach (var plugin in updatablePlugins) + { + try + { + notification.Content = $"Updating {plugin.InstalledPlugin.Manifest.Name}..."; + notification.Progress = (float)numDone / updatablePlugins.Count; + + if (this.isDryRun.Value) + { + await Task.Delay(5000); + } + + var status = await this.pluginManager.UpdateSinglePluginAsync(plugin, true, this.isDryRun.Value); + pluginStates.Add(status); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to auto-update plugin {PluginName}", plugin.InstalledPlugin.Manifest.Name); + } + + numDone++; + } + + notification.Progress = 1; + notification.UserDismissable = true; + notification.HardExpiry = DateTime.Now.AddSeconds(30); + + notification.DrawActions += _ => + { + ImGuiHelpers.ScaledDummy(2); + if (DalamudComponents.PrimaryButton("Open Plugin Installer")) + { + Service.Get().OpenPluginInstaller(); + notification.DismissNow(); + } + }; + + // Update the notification to show the final state + if (pluginStates.All(x => x.Status == PluginUpdateStatus.StatusKind.Success)) + { + notification.Minimized = true; + + // Janky way to make sure the notification does not change before it's minimized... + await Task.Delay(500); + + notification.Title = "Updates successful!"; + notification.MinimizedText = "Plugins updated successfully."; + notification.Type = NotificationType.Success; + notification.Content = "All plugins have been updated successfully."; + } + else + { + notification.Title = "Updates failed!"; + notification.Title = "Plugins failed to update."; + notification.Type = NotificationType.Error; + notification.Content = "Some plugins failed to update. Please check the plugin installer for more information."; + + var failedPlugins = pluginStates + .Where(x => x.Status != PluginUpdateStatus.StatusKind.Success) + .Select(x => x.Name).ToList(); + + notification.Content += $"\nFailed plugins: {string.Join(", ", failedPlugins)}"; + } + } + + private void NotifyUpdatesAreAvailable(ICollection updatablePlugins) + { + if (updatablePlugins.Count == 0) + return; + + var notification = this.GetBaseNotification(new Notification + { + Title = "Updates available!", + Content = $"There are {updatablePlugins.Count} plugins that can be updated.", + Type = NotificationType.Info, + InitialDuration = TimeSpan.MaxValue, + ShowIndeterminateIfNoExpiry = false, + Icon = INotificationIcon.From(FontAwesomeIcon.Download), + }); + + notification.DrawActions += _ => + { + ImGuiHelpers.ScaledDummy(2); + + if (DalamudComponents.PrimaryButton("Update")) + { + this.KickOffAutoUpdates(updatablePlugins); + notification.DismissNow(); + } + + ImGui.SameLine(); + if (DalamudComponents.SecondaryButton("Open installer")) + { + Service.Get().OpenPluginInstaller(); + notification.DismissNow(); + } + }; + } + + 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( + p => + !p.InstalledPlugin.IsDev && // Never update dev-plugins + p.InstalledPlugin.IsWantedByAnyProfile && // Never update plugins that are not wanted by any profile(not enabled) + !p.InstalledPlugin.Manifest.ScheduledForDeletion); // Never update plugins that we want to get rid of + + return updateablePlugins.Where(FilterPlugin).ToList(); + + 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; + + return true; + } + } + + private void OnLogin() + { + this.loginTime = DateTime.Now; + } + + private void OnLogout() + { + this.loginTime = null; + } + + private bool CanUpdateOrNag() + { + var condition = Service.Get(); + return this.IsPluginManagerReady() && + !this.dalamudInterface.IsPluginInstallerOpen && + condition.Only(ConditionFlag.NormalConditions, + ConditionFlag.Jumping, + ConditionFlag.Mounted, + ConditionFlag.UsingParasol); + } + + private bool IsPluginManagerReady() + { + return this.pluginManager.ReposReady && this.pluginManager.PluginsReady && !this.pluginManager.SafeMode; + } +} From bc2edf765f1b81adc93e2e8bc9d34fb71b0a9d30 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 18:35:47 +0200 Subject: [PATCH 12/23] refactor UpdatePluginsAsync() to take a list of plugins to update instead --- .../PluginInstaller/PluginInstallerWindow.cs | 6 ++- .../Internal/AutoUpdate/AutoUpdateManager.cs | 42 +++++-------------- Dalamud/Plugin/Internal/PluginManager.cs | 41 +++++++++++++++--- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 29b0253b8..d45f0b5ea 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -711,8 +711,12 @@ internal class PluginInstallerWindow : Window, IDisposable { this.updateStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.UpdatingAll; + + var toUpdate = this.pluginListUpdatable + .Where(x => x.InstalledPlugin.IsLoaded) + .ToList(); - Task.Run(() => pluginManager.UpdatePluginsAsync(true, false)) + Task.Run(() => pluginManager.UpdatePluginsAsync(toUpdate, false)) .ContinueWith(task => { this.updateStatus = OperationStatus.Complete; diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 3e12ef600..869f0c114 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -255,8 +255,6 @@ internal class AutoUpdateManager : IServiceType private async Task RunAutoUpdates(ICollection updatablePlugins) { - var pluginStates = new List(); - Log.Information("Found {UpdatablePluginsCount} plugins to update", updatablePlugins.Count); if (updatablePlugins.Count == 0) @@ -274,35 +272,16 @@ internal class AutoUpdateManager : IServiceType Icon = INotificationIcon.From(FontAwesomeIcon.Download), Minimized = false, }); - - var numDone = 0; - // TODO: This is NOT correct, we need to do this inside PM to be able to avoid notifying for each of these and avoid - // refreshing the plugin list until we're done. See PluginManager::UpdatePluginsAsync(). - // Maybe have a function in PM that can take a list of AvailablePluginUpdate instead and update them all, - // and get rid of UpdatePluginsAsync()? Will have to change the installer a bit but that might be for the better API-wise. - foreach (var plugin in updatablePlugins) + + var progress = new Progress(); + progress.ProgressChanged += (_, progress) => { - try - { - notification.Content = $"Updating {plugin.InstalledPlugin.Manifest.Name}..."; - notification.Progress = (float)numDone / updatablePlugins.Count; - - if (this.isDryRun.Value) - { - await Task.Delay(5000); - } - - var status = await this.pluginManager.UpdateSinglePluginAsync(plugin, true, this.isDryRun.Value); - pluginStates.Add(status); - } - catch (Exception ex) - { - Log.Error(ex, "Failed to auto-update plugin {PluginName}", plugin.InstalledPlugin.Manifest.Name); - } - - numDone++; - } + notification.Content = $"Updating {progress.CurrentPluginManifest.Name}..."; + notification.Progress = (float)progress.PluginsProcessed / progress.TotalPlugins; + }; + var pluginStates = await this.pluginManager.UpdatePluginsAsync(updatablePlugins, this.isDryRun.Value, true, progress); + notification.Progress = 1; notification.UserDismissable = true; notification.HardExpiry = DateTime.Now.AddSeconds(30); @@ -318,7 +297,8 @@ internal class AutoUpdateManager : IServiceType }; // Update the notification to show the final state - if (pluginStates.All(x => x.Status == PluginUpdateStatus.StatusKind.Success)) + var pluginUpdateStatusEnumerable = pluginStates as PluginUpdateStatus[] ?? pluginStates.ToArray(); + if (pluginUpdateStatusEnumerable.All(x => x.Status == PluginUpdateStatus.StatusKind.Success)) { notification.Minimized = true; @@ -337,7 +317,7 @@ internal class AutoUpdateManager : IServiceType notification.Type = NotificationType.Error; notification.Content = "Some plugins failed to update. Please check the plugin installer for more information."; - var failedPlugins = pluginStates + var failedPlugins = pluginUpdateStatusEnumerable .Where(x => x.Status != PluginUpdateStatus.StatusKind.Success) .Select(x => x.Name).ToList(); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 7517ae413..60d2bbe28 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -977,32 +977,39 @@ internal class PluginManager : IInternalDisposableService /// /// Update all non-dev plugins. /// - /// Ignore disabled plugins. + /// List of plugins to update. /// Perform a dry run, don't install anything. /// If this action was performed as part of an auto-update. + /// An implementation to receive progress updates about the installation status. /// Success or failure and a list of updated plugin metadata. - public async Task> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun, bool autoUpdate = false) + public async Task> UpdatePluginsAsync( + ICollection toUpdate, + bool dryRun, + bool autoUpdate = false, + IProgress? progress = null) { Log.Information("Starting plugin update"); var updateTasks = new List>(); + var totalPlugins = toUpdate.Count; + var processedPlugins = 0; // Prevent collection was modified errors lock (this.pluginListLock) { - foreach (var plugin in this.updatablePluginsList) + foreach (var plugin in toUpdate) { // Can't update that! if (plugin.InstalledPlugin.IsDev) continue; - if (!plugin.InstalledPlugin.IsWantedByAnyProfile && ignoreDisabled) + if (!plugin.InstalledPlugin.IsWantedByAnyProfile) continue; if (plugin.InstalledPlugin.Manifest.ScheduledForDeletion) continue; - updateTasks.Add(this.UpdateSinglePluginAsync(plugin, false, dryRun)); + updateTasks.Add(UpdateSinglePluginWithProgressAsync(plugin)); } } @@ -1013,9 +1020,26 @@ internal class PluginManager : IInternalDisposableService autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update, updatedList.Select(x => x.InternalName)); - Log.Information("Plugin update OK. {updateCount} plugins updated.", updatedList.Length); + Log.Information("Plugin update OK. {UpdateCount} plugins updated", updatedList.Length); return updatedList; + + async Task UpdateSinglePluginWithProgressAsync(AvailablePluginUpdate plugin) + { + var result = await this.UpdateSinglePluginAsync(plugin, false, dryRun); + + // Update the progress + if (progress != null) + { + var newProcessedAmount = Interlocked.Increment(ref processedPlugins); + progress.Report(new PluginUpdateProgress( + newProcessedAmount, + totalPlugins, + plugin.InstalledPlugin.Manifest)); + } + + return result; + } } /// @@ -1832,6 +1856,11 @@ internal class PluginManager : IInternalDisposableService } } + /// + /// Class representing progress of an update operation. + /// + public record PluginUpdateProgress(int PluginsProcessed, int TotalPlugins, IPluginManifest CurrentPluginManifest); + /// /// Simple class that tracks the internal names and public names of plugins that we are planning to load at startup, /// and are still actively loading. From 3b4178082a11be8cba245b436e74f052c7c4317e Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 18:54:01 +0200 Subject: [PATCH 13/23] localize all new auto-update strings --- .../Internal/AutoUpdate/AutoUpdateManager.cs | 73 ++++++++++++++----- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 869f0c114..d6d1165c3 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Threading.Tasks; +using CheapLoc; + using Dalamud.Configuration.Internal; using Dalamud.Console; using Dalamud.Game; @@ -22,8 +24,6 @@ using ImGuiNET; namespace Dalamud.Plugin.Internal.AutoUpdate; -// TODO: Loc - /// /// Class to manage automatic updates for plugins. /// @@ -262,8 +262,8 @@ internal class AutoUpdateManager : IServiceType var notification = this.GetBaseNotification(new Notification { - Title = "Updating plugins...", - Content = $"Preparing to update {updatablePlugins.Count} plugins...", + Title = Locs.NotificationTitleUpdatingPlugins, + Content = Locs.NotificationContentPreparingToUpdate(updatablePlugins.Count), Type = NotificationType.Info, InitialDuration = TimeSpan.MaxValue, ShowIndeterminateIfNoExpiry = false, @@ -276,7 +276,7 @@ internal class AutoUpdateManager : IServiceType var progress = new Progress(); progress.ProgressChanged += (_, progress) => { - notification.Content = $"Updating {progress.CurrentPluginManifest.Name}..."; + notification.Content = Locs.NotificationContentUpdating(progress.CurrentPluginManifest.Name); notification.Progress = (float)progress.PluginsProcessed / progress.TotalPlugins; }; @@ -289,7 +289,7 @@ internal class AutoUpdateManager : IServiceType notification.DrawActions += _ => { ImGuiHelpers.ScaledDummy(2); - if (DalamudComponents.PrimaryButton("Open Plugin Installer")) + if (DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller)) { Service.Get().OpenPluginInstaller(); notification.DismissNow(); @@ -305,23 +305,23 @@ internal class AutoUpdateManager : IServiceType // Janky way to make sure the notification does not change before it's minimized... await Task.Delay(500); - notification.Title = "Updates successful!"; - notification.MinimizedText = "Plugins updated successfully."; + notification.Title = Locs.NotificationTitleUpdatesSuccessful; + notification.MinimizedText = Locs.NotificationContentUpdatesSuccessfulMinimized; notification.Type = NotificationType.Success; - notification.Content = "All plugins have been updated successfully."; + notification.Content = Locs.NotificationContentUpdatesSuccessful; } else { - notification.Title = "Updates failed!"; - notification.Title = "Plugins failed to update."; + notification.Title = Locs.NotificationTitleUpdatesFailed; + notification.MinimizedText = Locs.NotificationContentUpdatesFailedMinimized; notification.Type = NotificationType.Error; - notification.Content = "Some plugins failed to update. Please check the plugin installer for more information."; + notification.Content = Locs.NotificationContentUpdatesFailed; var failedPlugins = pluginUpdateStatusEnumerable .Where(x => x.Status != PluginUpdateStatus.StatusKind.Success) .Select(x => x.Name).ToList(); - notification.Content += $"\nFailed plugins: {string.Join(", ", failedPlugins)}"; + notification.Content += "\n" + Locs.NotificationContentFailedPlugins(failedPlugins); } } @@ -332,8 +332,9 @@ internal class AutoUpdateManager : IServiceType var notification = this.GetBaseNotification(new Notification { - Title = "Updates available!", - Content = $"There are {updatablePlugins.Count} plugins that can be updated.", + Title = Locs.NotificationTitleUpdatesAvailable, + Content = Locs.NotificationContentUpdatesAvailable(updatablePlugins.Count), + MinimizedText = Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count), Type = NotificationType.Info, InitialDuration = TimeSpan.MaxValue, ShowIndeterminateIfNoExpiry = false, @@ -344,14 +345,14 @@ internal class AutoUpdateManager : IServiceType { ImGuiHelpers.ScaledDummy(2); - if (DalamudComponents.PrimaryButton("Update")) + if (DalamudComponents.PrimaryButton(Locs.NotificationButtonUpdate)) { this.KickOffAutoUpdates(updatablePlugins); notification.DismissNow(); } ImGui.SameLine(); - if (DalamudComponents.SecondaryButton("Open installer")) + if (DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller)) { Service.Get().OpenPluginInstaller(); notification.DismissNow(); @@ -416,4 +417,42 @@ internal class AutoUpdateManager : IServiceType { return this.pluginManager.ReposReady && this.pluginManager.PluginsReady && !this.pluginManager.SafeMode; } + + 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(int numUpdates) + => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "There are {0} plugins that can be updated."), numUpdates); + + public static string NotificationContentUpdatesAvailableMinimized(int numUpdates) + => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "{0} updates available."), numUpdates); + + public static string NotificationContentPreparingToUpdate(int numPlugins) + => string.Format(Loc.Localize("AutoUpdatePreparingToUpdate", "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 plugins: {0}"), string.Join(", ", failedPlugins)); + } } From ddcf01d073900b7f3b1e6ac85660eb8306d1cd1a Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 18:57:52 +0200 Subject: [PATCH 14/23] undo odd validity change i did without thinking about it --- .../Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs index c96163835..9c43f2f11 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs @@ -3,8 +3,6 @@ using CheapLoc; using Dalamud.Game.Text; using Dalamud.Interface.Internal.Windows.Settings.Widgets; -using Dalamud.Plugin.Internal.AutoUpdate; - namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] @@ -21,8 +19,9 @@ public class SettingsTabGeneral : SettingsTab Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."), c => c.GeneralChatType, (v, c) => c.GeneralChatType = v, - validity: (v) => + warning: v => { + // TODO: Maybe actually implement UI for the validity check... if (v == XivChatType.None) return Loc.Localize("DalamudSettingsChannelNone", "Do not pick \"None\"."); From 31ba979a831bed903cb5619c29e47e325875b0ff Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 19:29:13 +0200 Subject: [PATCH 15/23] fix warnings --- .../Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs index 9c43f2f11..d33bfacfb 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs @@ -3,6 +3,7 @@ using CheapLoc; using Dalamud.Game.Text; using Dalamud.Interface.Internal.Windows.Settings.Widgets; + namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] From c4e31bc5f165173902048c6d5a2f4796142fd9fc Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 19:29:23 +0200 Subject: [PATCH 16/23] use kaz's api for conditions instead --- .../Game/ClientState/Conditions/Condition.cs | 33 ++++++++++++++++--- .../Internal/AutoUpdate/AutoUpdateManager.cs | 2 +- Dalamud/Plugin/Services/ICondition.cs | 28 ++++++++++++---- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 23778288e..faafe05e0 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Dalamud.IoC; @@ -101,17 +102,33 @@ internal sealed class Condition : IInternalDisposableService, ICondition } /// - public bool Only(params ConditionFlag[] flags) + public bool OnlyAny(params ConditionFlag[] other) { + var resultSet = this.AsReadOnlySet(); + return !resultSet.Except(other).Any(); + } + + /// + public bool OnlyAll(params ConditionFlag[] other) + { + var resultSet = this.AsReadOnlySet(); + return resultSet.SetEquals(other); + } + + /// + public IReadOnlySet AsReadOnlySet() + { + var result = new HashSet(); + for (var i = 0; i < MaxConditionEntries; i++) { - if (this[i] && flags.All(f => (int)f != i)) + if (this[i]) { - return false; + result.Add((ConditionFlag)i); } } - return true; + return result; } private void Dispose(bool disposing) @@ -191,6 +208,9 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition this.ConditionChange = null; } + + /// + public IReadOnlySet AsReadOnlySet() => this.conditionService.AsReadOnlySet(); /// public bool Any() => this.conditionService.Any(); @@ -199,7 +219,10 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags); /// - public bool Only(params ConditionFlag[] flags) => this.conditionService.Only(flags); + public bool OnlyAny(params ConditionFlag[] other) => this.conditionService.OnlyAny(other); + + /// + public bool OnlyAll(params ConditionFlag[] other) => this.conditionService.OnlyAll(other); private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value); } diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index d6d1165c3..ce7b5af9d 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -407,7 +407,7 @@ internal class AutoUpdateManager : IServiceType var condition = Service.Get(); return this.IsPluginManagerReady() && !this.dalamudInterface.IsPluginInstallerOpen && - condition.Only(ConditionFlag.NormalConditions, + condition.OnlyAny(ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, ConditionFlag.UsingParasol); diff --git a/Dalamud/Plugin/Services/ICondition.cs b/Dalamud/Plugin/Services/ICondition.cs index 3b74c333c..7215cdac6 100644 --- a/Dalamud/Plugin/Services/ICondition.cs +++ b/Dalamud/Plugin/Services/ICondition.cs @@ -1,4 +1,6 @@ -using Dalamud.Game.ClientState.Conditions; +using System.Collections.Generic; + +using Dalamud.Game.ClientState.Conditions; namespace Dalamud.Plugin.Services; @@ -53,10 +55,24 @@ public interface ICondition public bool Any(params ConditionFlag[] flags); /// - /// Check if none but the provided condition flags are set. - /// This is not an exclusive check, it will return true if the provided flags are the only ones set. + /// Check that *only* any of the condition flags specified are set. Useful to test if the client is in one of any + /// of a few specific condiiton states. /// - /// The condition flags to check for. - /// Whether only flags passed in are set. - public bool Only(params ConditionFlag[] flags); + /// The array of flags to check. + /// Returns a bool. + public bool OnlyAny(params ConditionFlag[] other); + + /// + /// Check that *only* the specified flags are set. Unlike , this method requires that all the + /// specified flags are set and no others are present. + /// + /// The array of flags to check. + /// Returns a bool. + public bool OnlyAll(params ConditionFlag[] other); + + /// + /// Convert the conditions array to a set of all set condition flags. + /// + /// Returns a set. + public IReadOnlySet AsReadOnlySet(); } From 9baf0905ec06bd5f5f80eec51c92c9012c989bd5 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 19:40:11 +0200 Subject: [PATCH 17/23] don't ask for auto-updates twice in changelog --- .../Internal/Windows/ChangelogWindow.cs | 58 +++++++++++++------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 613fc7d28..665ad0f55 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -17,6 +17,7 @@ using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Storage.Assets; using Dalamud.Utility; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows; @@ -26,8 +27,6 @@ namespace Dalamud.Interface.Internal.Windows; /// internal sealed class ChangelogWindow : Window, IDisposable { - private const AutoUpdateBehavior DefaultAutoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo; - private const string WarrantsChangelogForMajorMinor = "9.0."; private const string ChangeLog = @@ -77,7 +76,10 @@ internal sealed class ChangelogWindow : Window, IDisposable private bool isFadingOutForStateChange = false; private State? stateAfterFadeOut; - private AutoUpdateBehavior autoUpdateBehavior = DefaultAutoUpdateBehavior; + private AutoUpdateBehavior? chosenAutoUpdateBehavior; + + private int currentFtueLevel; + private int updatedFtueLevel; /// /// Initializes a new instance of the class. @@ -147,8 +149,10 @@ internal sealed class ChangelogWindow : Window, IDisposable this.titleFade.Reset(); this.fadeOut.Reset(); this.needFadeRestart = true; + + this.chosenAutoUpdateBehavior = null; - this.autoUpdateBehavior = DefaultAutoUpdateBehavior; + this.currentFtueLevel = Service.Get().SeenFtueLevel; base.OnOpen(); } @@ -162,7 +166,13 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(false); var configuration = Service.Get(); - configuration.AutoUpdateBehavior = this.autoUpdateBehavior; + + if (this.chosenAutoUpdateBehavior.HasValue) + { + configuration.AutoUpdateBehavior = this.chosenAutoUpdateBehavior.Value; + } + + configuration.SeenFtueLevel = this.updatedFtueLevel; configuration.QueueSave(); } @@ -302,7 +312,7 @@ internal sealed class ChangelogWindow : Window, IDisposable this.fadeOut.Restart(); } - void DrawNextButton(State nextState) + bool DrawNextButton(State nextState) { // Draw big, centered next button at the bottom of the window var buttonHeight = 30 * ImGuiHelpers.GlobalScale; @@ -314,7 +324,10 @@ internal sealed class ChangelogWindow : Window, IDisposable if (ImGui.Button(buttonText, new Vector2(buttonWidth, buttonHeight)) && !this.isFadingOutForStateChange) { GoToNextState(nextState); + return true; } + + return false; } switch (this.state) @@ -347,7 +360,18 @@ internal sealed class ChangelogWindow : Window, IDisposable this.apiBumpExplainerTexture.Value.ImGuiHandle, this.apiBumpExplainerTexture.Value.Size); - DrawNextButton(State.AskAutoUpdate); + if (this.currentFtueLevel < FtueLevels.AutoUpdateBehavior) + { + if (DrawNextButton(State.AskAutoUpdate)) + { + this.updatedFtueLevel = this.currentFtueLevel = FtueLevels.AutoUpdateBehavior; + } + } + else + { + DrawNextButton(State.Links); + } + break; case State.AskAutoUpdate: @@ -364,16 +388,6 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGuiHelpers.ScaledDummy(15); - /* - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior", - "When the game starts...")); - var behaviorInt = (int)this.autoUpdateBehavior; - ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNone", "Do not check for updates automatically"), ref behaviorInt, (int)AutoUpdateBehavior.None); - ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNotify", "Only notify me of new updates"), ref behaviorInt, (int)AutoUpdateBehavior.OnlyNotify); - ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateMainRepo", "Auto-update main repository plugins"), ref behaviorInt, (int)AutoUpdateBehavior.UpdateMainRepo); - this.autoUpdateBehavior = (AutoUpdateBehavior)behaviorInt; - */ - bool DrawCenteredButton(string text, float height) { var buttonHeight = height * ImGuiHelpers.GlobalScale; @@ -388,7 +402,7 @@ internal sealed class ChangelogWindow : Window, IDisposable { if (DrawCenteredButton("Enable auto-updates", 30)) { - this.autoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo; + this.chosenAutoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo; GoToNextState(State.Links); } } @@ -401,7 +415,7 @@ internal sealed class ChangelogWindow : Window, IDisposable buttonColor.Push(ImGuiCol.Border, ImGuiColors.DalamudGrey3); if (DrawCenteredButton("Disable auto-updates", 25)) { - this.autoUpdateBehavior = AutoUpdateBehavior.OnlyNotify; + this.chosenAutoUpdateBehavior = AutoUpdateBehavior.OnlyNotify; GoToNextState(State.Links); } } @@ -503,4 +517,10 @@ internal sealed class ChangelogWindow : Window, IDisposable public void Dispose() { } + + private static class FtueLevels + { + public const int Default = 1; + public const int AutoUpdateBehavior = 2; + } } From 408c8e5d02ae83cfc0f2e20ab90ce0193cdeffda Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 21:49:16 +0200 Subject: [PATCH 18/23] remove double-negative --- Dalamud/Interface/Internal/Windows/ChangelogWindow.cs | 2 +- .../Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 665ad0f55..16e70d9e7 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -384,7 +384,7 @@ internal sealed class ChangelogWindow : Window, IDisposable "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 never bother you about updates while you are not idle.")); + "Dalamud will only notify you about updates while you are idle.")); ImGuiHelpers.ScaledDummy(15); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs index 0b18e59d9..5ed2c528c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -41,7 +41,7 @@ public class SettingsTabAutoUpdates : SettingsTab "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 never bother you about updates while you are not idle.")); + "Dalamud will only notify you about updates while you are idle.")); ImGuiHelpers.ScaledDummy(8); From ef72ebc72c943d87470d734ec5379b756cb903ae Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 22:03:17 +0200 Subject: [PATCH 19/23] more wording changes --- .../Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs index 5ed2c528c..77c79c96d 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -87,7 +87,7 @@ public class SettingsTabAutoUpdates : SettingsTab using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) { ImGuiHelpers.CenteredText(Loc.Localize("DalamudSettingsAutoUpdateOptedInHint2", - "You did not override auto-updates for any plugins yet.")); + "You don't have auto-update rules for any plugins.")); } ImGuiHelpers.ScaledDummy(2); From 08a411728cc5728d72c8465943f83dd71241f625 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 22:08:30 +0200 Subject: [PATCH 20/23] use a map to track ftue levels per-feature instead --- .../Internal/DalamudConfiguration.cs | 5 ++--- .../Internal/Windows/ChangelogWindow.cs | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index e5348d999..ca029307d 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -80,10 +80,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public string? LastVersion { get; set; } = null; /// - /// Gets or sets a value indicating the last seen FTUE version. - /// Unused for now, added to prevent existing users from seeing level 0 FTUE. + /// Gets or sets a dictionary of seen FTUE levels. /// - public int SeenFtueLevel { get; set; } = 1; + public Dictionary SeenFtueLevels { get; set; } = new(); /// /// Gets or sets the last loaded Dalamud version. diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 16e70d9e7..906fda2e4 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -78,8 +79,7 @@ internal sealed class ChangelogWindow : Window, IDisposable private AutoUpdateBehavior? chosenAutoUpdateBehavior; - private int currentFtueLevel; - private int updatedFtueLevel; + private Dictionary currentFtueLevels = new(); /// /// Initializes a new instance of the class. @@ -152,7 +152,7 @@ internal sealed class ChangelogWindow : Window, IDisposable this.chosenAutoUpdateBehavior = null; - this.currentFtueLevel = Service.Get().SeenFtueLevel; + this.currentFtueLevels = Service.Get().SeenFtueLevels; base.OnOpen(); } @@ -172,7 +172,7 @@ internal sealed class ChangelogWindow : Window, IDisposable configuration.AutoUpdateBehavior = this.chosenAutoUpdateBehavior.Value; } - configuration.SeenFtueLevel = this.updatedFtueLevel; + configuration.SeenFtueLevels = this.currentFtueLevels; configuration.QueueSave(); } @@ -360,11 +360,11 @@ internal sealed class ChangelogWindow : Window, IDisposable this.apiBumpExplainerTexture.Value.ImGuiHandle, this.apiBumpExplainerTexture.Value.Size); - if (this.currentFtueLevel < FtueLevels.AutoUpdateBehavior) + if (!this.currentFtueLevels.TryGetValue(FtueLevels.AutoUpdate.Name, out var autoUpdateLevel) || autoUpdateLevel < FtueLevels.AutoUpdate.AutoUpdateInitial) { if (DrawNextButton(State.AskAutoUpdate)) { - this.updatedFtueLevel = this.currentFtueLevel = FtueLevels.AutoUpdateBehavior; + this.currentFtueLevels[FtueLevels.AutoUpdate.Name] = FtueLevels.AutoUpdate.AutoUpdateInitial; } } else @@ -520,7 +520,10 @@ internal sealed class ChangelogWindow : Window, IDisposable private static class FtueLevels { - public const int Default = 1; - public const int AutoUpdateBehavior = 2; + public static class AutoUpdate + { + public const string Name = "AutoUpdate"; + public const int AutoUpdateInitial = 1; + } } } From 13fc380dd515912d246273164b4c661b391aa0cf Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 23:09:08 +0200 Subject: [PATCH 21/23] disable autoupdate dry run by default --- Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index ce7b5af9d..4e2179be8 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -87,7 +87,7 @@ internal class AutoUpdateManager : IServiceType }); Service.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; }); - this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", true); + this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", false); console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () => { this.hasStartedInitialUpdateThisSession = false; From 8f36641f36a1212bc2dacb78eb3fda1a357ba2d9 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 16 Jun 2024 00:07:28 +0200 Subject: [PATCH 22/23] add special indicator for testing and testing-exclusive plugins --- .../PluginInstaller/PluginInstallerWindow.cs | 114 +++++++++++++++--- 1 file changed, 95 insertions(+), 19 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index d45f0b5ea..1033c9ea4 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -207,6 +207,19 @@ internal class PluginInstallerWindow : Window, IDisposable EnabledDisabled, ProfileOrNot, } + + [Flags] + private enum PluginHeaderFlags + { + None = 0, + IsThirdParty = 1 << 0, + HasTrouble = 1 << 1, + UpdateAvailable = 1 << 2, + IsNew = 1 << 3, + IsInstallableOutdated = 1 << 4, + IsOrphan = 1 << 5, + IsTesting = 1 << 6, + } private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress || this.updateStatus == OperationStatus.InProgress || @@ -1778,22 +1791,62 @@ internal class PluginInstallerWindow : Window, IDisposable return ready; } - private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, bool trouble, bool updateAvailable, bool isNew, bool installableOutdated, bool isOrphan, Action drawContextMenuAction, int index) + private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, IPluginManifest manifest, PluginHeaderFlags flags, Action drawContextMenuAction, int index) { - ImGui.Separator(); - var isOpen = this.openPluginCollapsibles.Contains(index); var sectionSize = ImGuiHelpers.GlobalScale * 66; + var tapeCursor = ImGui.GetCursorPos(); + + ImGui.Separator(); + var startCursor = ImGui.GetCursorPos(); + if (flags.HasFlag(PluginHeaderFlags.IsTesting)) + { + void DrawCautionTape(Vector2 position, Vector2 size, float stripeWidth, float skewAmount) + { + var wdl = ImGui.GetWindowDrawList(); + + var windowPos = ImGui.GetWindowPos(); + var scroll = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY()); + + var adjustedPosition = windowPos + position - scroll; + + var yellow = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.9f, 0.0f, 0.10f)); + var numStripes = (int)(size.X / stripeWidth) + (int)(size.Y / skewAmount) + 1; // +1 to cover partial stripe + + for (var i = 0; i < numStripes; i++) + { + var x0 = adjustedPosition.X + i * stripeWidth; + var x1 = x0 + stripeWidth; + var y0 = adjustedPosition.Y; + var y1 = y0 + size.Y; + + var p0 = new Vector2(x0, y0); + var p1 = new Vector2(x1, y0); + var p2 = new Vector2(x1 - skewAmount, y1); + var p3 = new Vector2(x0 - skewAmount, y1); + + if (i % 2 != 0) + continue; + + wdl.AddQuadFilled(p0, p1, p2, p3, yellow); + } + } + + DrawCautionTape(tapeCursor + new Vector2(0, 1), new Vector2(ImGui.GetWindowWidth(), sectionSize + ImGui.GetStyle().ItemSpacing.Y), ImGuiHelpers.GlobalScale * 40, 20); + } + ImGui.PushStyleColor(ImGuiCol.Button, isOpen ? new Vector4(0.5f, 0.5f, 0.5f, 0.1f) : Vector4.Zero); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0.5f, 0.5f, 0.5f, 0.2f)); ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0.5f, 0.5f, 0.5f, 0.35f)); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0); + + ImGui.SetCursorPos(tapeCursor); - if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetContentRegionAvail().X, sectionSize))) + if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetContentRegionAvail().X, sectionSize + ImGui.GetStyle().ItemSpacing.Y))) { if (isOpen) { @@ -1825,7 +1878,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize)) { var iconTex = this.imageCache.DefaultIcon; - var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, isThirdParty, out var cachedIconTex, out var loadedSince); + var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, flags.HasFlag(PluginHeaderFlags.IsThirdParty), out var cachedIconTex, out var loadedSince); if (hasIcon && cachedIconTex != null) { iconTex = cachedIconTex; @@ -1839,7 +1892,7 @@ internal class PluginInstallerWindow : Window, IDisposable float EaseOutCubic(float t) => 1 - MathF.Pow(1 - t, 3); var secondsSinceLoad = (float)DateTime.Now.Subtract(loadedSince.Value).TotalSeconds; - var fadeTo = pluginDisabled || installableOutdated ? 0.4f : 1f; + var fadeTo = pluginDisabled || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated) ? 0.4f : 1f; float Interp(float to) => Math.Clamp(EaseOutCubic(Math.Min(secondsSinceLoad, fadeTime) / fadeTime) * to, 0, 1); iconAlpha = Interp(fadeTo); @@ -1857,11 +1910,11 @@ internal class PluginInstallerWindow : Window, IDisposable var isLoaded = plugin is { IsLoaded: true }; ImGui.PushStyleVar(ImGuiStyleVar.Alpha, overlayAlpha); - if (updateAvailable) + if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); - else if ((trouble && !pluginDisabled) || isOrphan) + else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan)) ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize); - else if (installableOutdated) + else if (flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) ImGui.Image(this.imageCache.OutdatedInstallableIcon.ImGuiHandle, iconSize); else if (pluginDisabled) ImGui.Image(this.imageCache.DisabledIcon.ImGuiHandle, iconSize); @@ -1905,7 +1958,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.DrawFontawesomeIconOutlined(FontAwesomeIcon.Wrench, devIconOutlineColor, devIconColor); this.VerifiedCheckmarkFadeTooltip(label, "This is a dev plugin. You added it."); } - else if (!isThirdParty) + else if (!flags.HasFlag(PluginHeaderFlags.IsThirdParty)) { this.DrawFontawesomeIconOutlined(FontAwesomeIcon.CheckCircle, verifiedOutlineColor, verifiedIconColor); this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_VerifiedTooltip); @@ -1925,7 +1978,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SameLine(); ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadCountText); - if (isNew) + if (flags.HasFlag(PluginHeaderFlags.IsNew)) { ImGui.SameLine(); ImGui.TextColored(ImGuiColors.TankBlue, Locs.PluginTitleMod_New); @@ -1935,12 +1988,12 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SetCursorPos(cursor); // Outdated warning - if (plugin is { IsOutdated: true, IsBanned: false } || installableOutdated) + if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); var bodyText = Locs.PluginBody_Outdated + " "; - if (updateAvailable) + if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) bodyText += Locs.PluginBody_Outdated_CanNowUpdate; else bodyText += Locs.PluginBody_Outdated_WaitForUpdate; @@ -1958,7 +2011,7 @@ internal class PluginInstallerWindow : Window, IDisposable : Locs.PluginBody_BannedReason(plugin.BanReason); bodyText += " "; - if (updateAvailable) + if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) bodyText += Locs.PluginBody_Outdated_CanNowUpdate; else bodyText += Locs.PluginBody_Outdated_WaitForUpdate; @@ -2002,7 +2055,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SetCursorPosX(cursor.X); // Description - if (plugin is null or { IsOutdated: false, IsBanned: false } && !trouble) + if (plugin is null or { IsOutdated: false, IsBanned: false } && !flags.HasFlag(PluginHeaderFlags.HasTrouble)) { if (!string.IsNullOrWhiteSpace(manifest.Punchline)) { @@ -2123,11 +2176,22 @@ internal class PluginInstallerWindow : Window, IDisposable { label += Locs.PluginTitleMod_TestingAvailable; } + + var isThirdParty = manifest.SourceRepo.IsThirdParty; ImGui.PushID($"available{index}{manifest.InternalName}"); - - var isThirdParty = manifest.SourceRepo.IsThirdParty; - if (this.DrawPluginCollapsingHeader(label, null, manifest, isThirdParty, false, false, !wasSeen, isOutdated, false, () => this.DrawAvailablePluginContextMenu(manifest), index)) + + var flags = PluginHeaderFlags.None; + if (isThirdParty) + flags |= PluginHeaderFlags.IsThirdParty; + if (!wasSeen) + flags |= PluginHeaderFlags.IsNew; + if (isOutdated) + flags |= PluginHeaderFlags.IsInstallableOutdated; + if (useTesting || manifest.IsTestingExclusive) + flags |= PluginHeaderFlags.IsTesting; + + if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index)) { if (!wasSeen) configuration.SeenPluginInternalName.Add(manifest.InternalName); @@ -2391,7 +2455,19 @@ internal class PluginInstallerWindow : Window, IDisposable var hasChangelog = !applicableChangelog.IsNullOrWhitespace(); var didDrawChangelogInsideCollapsible = false; - if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index)) + var flags = PluginHeaderFlags.None; + if (plugin.IsThirdParty) + flags |= PluginHeaderFlags.IsThirdParty; + if (trouble) + flags |= PluginHeaderFlags.HasTrouble; + if (availablePluginUpdate != default) + flags |= PluginHeaderFlags.UpdateAvailable; + if (plugin.IsOrphaned) + flags |= PluginHeaderFlags.IsOrphan; + if (plugin.IsTesting) + flags |= PluginHeaderFlags.IsTesting; + + if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, flags, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index)) { if (!this.WasPluginSeen(plugin.Manifest.InternalName)) configuration.SeenPluginInternalName.Add(plugin.Manifest.InternalName); From 1c03242aa97a555a70346bd3fa4314b2e4d7fc39 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 15 Jun 2024 15:44:47 -0700 Subject: [PATCH 23/23] feat: New Condition APIs (#1842) * feat: New Condition APIs - New methods for `ICondition`: - `AnyExcept` ensures the listed conditions are *not* present. - `OnlyAny` ensures that *only* the listed conditions are met. - `EqualTo` ensures that the condition state matches the listed set. - New `IsGameIdle` method in `IClientState` can be used to check if the player is not in any "active" game state. --- Dalamud/Game/ClientState/ClientState.cs | 29 +++++++++++ .../Game/ClientState/Conditions/Condition.cs | 48 +++++++++++-------- Dalamud/Plugin/Services/IClientState.cs | 16 +++++++ Dalamud/Plugin/Services/ICondition.cs | 24 ++++++---- 4 files changed, 88 insertions(+), 29 deletions(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 2e8d128c3..5bc49abc8 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -1,6 +1,8 @@ +using System.Linq; using System.Runtime.InteropServices; using Dalamud.Data; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Gui; @@ -123,6 +125,27 @@ internal sealed class ClientState : IInternalDisposableService, IClientState /// Gets client state address resolver. /// internal ClientStateAddressResolver AddressResolver => this.address; + + /// + public bool IsClientIdle(out ConditionFlag blockingFlag) + { + blockingFlag = 0; + if (this.LocalPlayer is null) return true; + + var condition = Service.GetNullable(); + + var blockingConditions = condition.AsReadOnlySet().Except([ + ConditionFlag.NormalConditions, + ConditionFlag.Jumping, + ConditionFlag.Mounted, + ConditionFlag.UsingParasol]); + + blockingFlag = blockingConditions.FirstOrDefault(); + return blockingFlag == 0; + } + + /// + public bool IsClientIdle() => this.IsClientIdle(out _); /// /// Dispose of managed and unmanaged resources. @@ -271,6 +294,12 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat /// public bool IsGPosing => this.clientStateService.IsGPosing; + /// + public bool IsClientIdle(out ConditionFlag blockingFlag) => this.clientStateService.IsClientIdle(out blockingFlag); + + /// + public bool IsClientIdle() => this.clientStateService.IsClientIdle(); + /// void IInternalDisposableService.DisposeService() { diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index faafe05e0..b5cb77275 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -71,6 +71,22 @@ internal sealed class Condition : IInternalDisposableService, ICondition /// void IInternalDisposableService.DisposeService() => this.Dispose(true); + + /// + public IReadOnlySet AsReadOnlySet() + { + var result = new HashSet(); + + for (var i = 0; i < MaxConditionEntries; i++) + { + if (this[i]) + { + result.Add((ConditionFlag)i); + } + } + + return result; + } /// public bool Any() @@ -100,37 +116,26 @@ internal sealed class Condition : IInternalDisposableService, ICondition return false; } + + /// + public bool AnyExcept(params ConditionFlag[] excluded) + { + return !this.AsReadOnlySet().Intersect(excluded).Any(); + } /// public bool OnlyAny(params ConditionFlag[] other) { - var resultSet = this.AsReadOnlySet(); - return !resultSet.Except(other).Any(); + return !this.AsReadOnlySet().Except(other).Any(); } /// - public bool OnlyAll(params ConditionFlag[] other) + public bool EqualTo(params ConditionFlag[] other) { var resultSet = this.AsReadOnlySet(); return resultSet.SetEquals(other); } - /// - public IReadOnlySet AsReadOnlySet() - { - var result = new HashSet(); - - for (var i = 0; i < MaxConditionEntries; i++) - { - if (this[i]) - { - result.Add((ConditionFlag)i); - } - } - - return result; - } - private void Dispose(bool disposing) { if (this.isDisposed) @@ -217,12 +222,15 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition /// public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags); + + /// + public bool AnyExcept(params ConditionFlag[] except) => this.conditionService.AnyExcept(except); /// public bool OnlyAny(params ConditionFlag[] other) => this.conditionService.OnlyAny(other); /// - public bool OnlyAll(params ConditionFlag[] other) => this.conditionService.OnlyAll(other); + public bool EqualTo(params ConditionFlag[] other) => this.conditionService.EqualTo(other); private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value); } diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 95d5259e6..089a78d42 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -1,3 +1,4 @@ +using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; namespace Dalamud.Plugin.Services; @@ -81,4 +82,19 @@ public interface IClientState /// Gets a value indicating whether the client is currently in Group Pose (GPose) mode. /// public bool IsGPosing { get; } + + /// + /// Check whether the client is currently "idle". This means a player is not logged in, or is notctively in combat + /// or doing anything that we may not want to disrupt. + /// + /// An outvar containing the first observed condition blocking the "idle" state. 0 if idle. + /// Returns true if the client is idle, false otherwise. + public bool IsClientIdle(out ConditionFlag blockingFlag); + + /// + /// Check whether the client is currently "idle". This means a player is not logged in, or is notctively in combat + /// or doing anything that we may not want to disrupt. + /// + /// Returns true if the client is idle, false otherwise. + public bool IsClientIdle() => this.IsClientIdle(out _); } diff --git a/Dalamud/Plugin/Services/ICondition.cs b/Dalamud/Plugin/Services/ICondition.cs index 7215cdac6..4ea9e7f76 100644 --- a/Dalamud/Plugin/Services/ICondition.cs +++ b/Dalamud/Plugin/Services/ICondition.cs @@ -40,6 +40,12 @@ public interface ICondition /// public bool this[ConditionFlag flag] => this[(int)flag]; + + /// + /// Convert the conditions array to a set of all set condition flags. + /// + /// Returns a set. + public IReadOnlySet AsReadOnlySet(); /// /// Check if any condition flags are set. @@ -55,8 +61,14 @@ public interface ICondition public bool Any(params ConditionFlag[] flags); /// - /// Check that *only* any of the condition flags specified are set. Useful to test if the client is in one of any - /// of a few specific condiiton states. + /// Check that the specified condition flags are *not* present in the current conditions. + /// + /// The array of flags to check. + /// Returns false if any of the listed conditions are present, true otherwise. + public bool AnyExcept(params ConditionFlag[] except); + + /// + /// Check that *only* any of the condition flags specified are set. /// /// The array of flags to check. /// Returns a bool. @@ -68,11 +80,5 @@ public interface ICondition /// /// The array of flags to check. /// Returns a bool. - public bool OnlyAll(params ConditionFlag[] other); - - /// - /// Convert the conditions array to a set of all set condition flags. - /// - /// Returns a set. - public IReadOnlySet AsReadOnlySet(); + public bool EqualTo(params ConditionFlag[] other); }