Merge pull request #1841 from goaaats/feature/new_autoupdates

New auto-update UX
This commit is contained in:
goat 2024-06-15 23:10:18 +02:00 committed by GitHub
commit 0127dc6e98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1269 additions and 243 deletions

View file

@ -0,0 +1,42 @@
namespace Dalamud.Configuration.Internal;
/// <summary>
/// Class representing a plugin that has opted in to auto-updating.
/// </summary>
internal class AutoUpdatePreference
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoUpdatePreference"/> class.
/// </summary>
/// <param name="pluginId">The unique ID representing the plugin.</param>
public AutoUpdatePreference(Guid pluginId)
{
this.WorkingPluginId = pluginId;
}
/// <summary>
/// The kind of opt-in.
/// </summary>
public enum OptKind
{
/// <summary>
/// Never auto-update this plugin.
/// </summary>
NeverUpdate,
/// <summary>
/// Always auto-update this plugin, regardless of the user's settings.
/// </summary>
AlwaysUpdate,
}
/// <summary>
/// Gets or sets the unique ID representing the plugin.
/// </summary>
public Guid WorkingPluginId { get; set; }
/// <summary>
/// Gets or sets the type of opt-in.
/// </summary>
public OptKind Kind { get; set; } = OptKind.AlwaysUpdate;
}

View file

@ -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;
@ -80,10 +80,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public string? LastVersion { get; set; } = null;
/// <summary>
/// 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.
/// </summary>
public int SeenFtueLevel { get; set; } = 1;
public Dictionary<string, int> SeenFtueLevels { get; set; } = new();
/// <summary>
/// Gets or sets the last loaded Dalamud version.
@ -196,6 +195,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a value indicating whether or not plugins should be auto-updated.
/// </summary>
[Obsolete("Use AutoUpdateBehavior instead.")]
public bool AutoUpdatePlugins { get; set; }
/// <summary>
@ -229,7 +229,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public int LogLinesLimit { get; set; } = 10000;
/// <summary>
/// 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.
/// </summary>
public List<string> LogCommandHistory { get; set; } = new();
@ -434,7 +434,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a list of plugins that testing builds should be downloaded for.
/// </summary>
public List<PluginTestingOptIn>? PluginTestingOptIns { get; set; }
public List<PluginTestingOptIn> PluginTestingOptIns { get; set; } = [];
/// <summary>
/// Gets or sets a list of plugins that have opted into or out of auto-updating.
/// </summary>
public List<AutoUpdatePreference> PluginAutoUpdatePreferences { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether the FFXIV window should be toggled to immersive mode.
@ -466,6 +471,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public PluginInstallerOpenKind PluginInstallerOpen { get; set; } = PluginInstallerOpenKind.AllPlugins;
/// <summary>
/// Gets or sets a value indicating how auto-updating should behave.
/// </summary>
public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null;
/// <summary>
/// Gets or sets a value indicating whether or not users should be notified regularly about pending updates.
/// </summary>
public bool CheckPeriodicallyForUpdates { get; set; } = true;
/// <summary>
/// Load a configuration from the provided path.
/// </summary>
@ -551,6 +566,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
private void SetDefaults()
{
#pragma warning disable CS0618
// "Reduced motion"
if (!this.ReduceMotions.HasValue)
{
@ -572,6 +588,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()

View file

@ -31,7 +31,6 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
/// Initializes a new instance of the <see cref="ConsoleManagerPluginScoped"/> class.
/// </summary>
/// <param name="plugin">The plugin this service belongs to.</param>
/// <param name="console">The console manager.</param>
[ServiceManager.ServiceConstructor]
internal ConsoleManagerPluginScoped(LocalPlugin plugin)
{

View file

@ -30,40 +30,6 @@ namespace Dalamud.Game;
[ServiceManager.EarlyLoadedService]
internal class ChatHandlers : IServiceType
{
// private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new()
// {
// { "", "<:ffxive071:585847382210642069>" },
// { "", "<:ffxive083:585848592699490329>" },
// };
// private readonly Dictionary<XivChatType, Color> 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<Dalamud>.Get();
@ -115,19 +79,12 @@ internal class ChatHandlers : IServiceType
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.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<DalamudInterface>.GetNullable()?.OpenPluginInstallerTo(PluginInstallerOpenKind.InstalledPlugins);
});
}
/// <summary>
@ -135,11 +92,6 @@ internal class ChatHandlers : IServiceType
/// </summary>
public string? LastLink { get; private set; }
/// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session.
/// </summary>
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<ChatGui>.GetNullable();
var pluginManager = Service<PluginManager>.GetNullable();
var notifications = Service<NotificationManager>.GetNullable();
var condition = Service<Condition>.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<PluginManager>.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<Payload>()
{
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;
}
}

View file

@ -1,3 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
@ -98,6 +101,36 @@ internal sealed class Condition : IInternalDisposableService, ICondition
return false;
}
/// <inheritdoc/>
public bool OnlyAny(params ConditionFlag[] other)
{
var resultSet = this.AsReadOnlySet();
return !resultSet.Except(other).Any();
}
/// <inheritdoc/>
public bool OnlyAll(params ConditionFlag[] other)
{
var resultSet = this.AsReadOnlySet();
return resultSet.SetEquals(other);
}
/// <inheritdoc/>
public IReadOnlySet<ConditionFlag> AsReadOnlySet()
{
var result = new HashSet<ConditionFlag>();
for (var i = 0; i < MaxConditionEntries; i++)
{
if (this[i])
{
result.Add((ConditionFlag)i);
}
}
return result;
}
private void Dispose(bool disposing)
{
if (this.isDisposed)
@ -175,12 +208,21 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition
this.ConditionChange = null;
}
/// <inheritdoc/>
public IReadOnlySet<ConditionFlag> AsReadOnlySet() => this.conditionService.AsReadOnlySet();
/// <inheritdoc/>
public bool Any() => this.conditionService.Any();
/// <inheritdoc/>
public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags);
/// <inheritdoc/>
public bool OnlyAny(params ConditionFlag[] other) => this.conditionService.OnlyAny(other);
/// <inheritdoc/>
public bool OnlyAll(params ConditionFlag[] other) => this.conditionService.OnlyAll(other);
private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value);
}

View file

@ -35,6 +35,11 @@ public enum SettingsOpenKind
/// Open to the "Look &#038; Feel" page.
/// </summary>
LookAndFeel,
/// <summary>
/// Open to the "Auto Updates" page.
/// </summary>
AutoUpdates,
/// <summary>
/// Open to the "Server Info Bar" page.

View file

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

View file

@ -211,6 +211,15 @@ internal class DalamudInterface : IInternalDisposableService
set => this.isImGuiDrawDevMenu = value;
}
/// <summary>
/// Gets or sets a value indicating whether the plugin installer is open.
/// </summary>
public bool IsPluginInstallerOpen
{
get => this.pluginWindow.IsOpen;
set => this.pluginWindow.IsOpen = value;
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{

View file

@ -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;
/// <summary>
/// Private ImGui widgets for use inside Dalamud.
/// </summary>
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;
/// <summary>
/// Draw a "primary style" button.
/// </summary>
/// <param name="text">The text to show.</param>
/// <returns>True if the button was clicked.</returns>
internal static bool PrimaryButton(string text)
{
using (ImRaii.PushColor(ImGuiCol.Button, PrimaryButtonBackground))
{
return Button(text);
}
}
/// <summary>
/// Draw a "secondary style" button.
/// </summary>
/// <param name="text">The text to show.</param>
/// <returns>True if the button was clicked.</returns>
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);
}
}
}

View file

@ -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;
/// <summary>
/// Private ImGui widgets for use inside Dalamud.
/// </summary>
internal static partial class DalamudComponents
{
/// <summary>
/// Draw a "picker" popup to chose a plugin.
/// </summary>
/// <param name="id">The ID of the popup.</param>
/// <param name="pickerSearch">String holding the search input.</param>
/// <param name="onClicked">Action to be called if a plugin is clicked.</param>
/// <param name="pluginDisabled">Function that should return true if a plugin should show as disabled.</param>
/// <param name="pluginFiltered">Function that should return true if a plugin should not appear in the list.</param>
/// <returns>An ImGuiID to open the popup.</returns>
internal static uint DrawPluginPicker(string id, ref string pickerSearch, Action<LocalPlugin> onClicked, Func<LocalPlugin, bool> pluginDisabled, Func<LocalPlugin, bool>? pluginFiltered = null)
{
var pm = Service<PluginManager>.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...");
}
}

View file

@ -0,0 +1,8 @@
namespace Dalamud.Interface.Internal.DesignSystem;
/// <summary>
/// Private ImGui widgets for use inside Dalamud.
/// </summary>
internal static partial class DalamudComponents
{
}

View file

@ -1,6 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors;
@ -12,8 +15,10 @@ 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;
namespace Dalamud.Interface.Internal.Windows;
@ -47,15 +52,34 @@ 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? chosenAutoUpdateBehavior;
private Dictionary<string, int> currentFtueLevels = new();
/// <summary>
/// Initializes a new instance of the <see cref="ChangelogWindow"/> class.
@ -90,6 +114,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
WindowFadeIn,
ExplainerIntro,
ExplainerApiBump,
AskAutoUpdate,
Links,
}
@ -114,11 +139,20 @@ 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.chosenAutoUpdateBehavior = null;
this.currentFtueLevels = Service<DalamudConfiguration>.Get().SeenFtueLevels;
base.OnOpen();
}
@ -130,6 +164,16 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.tsmWindow.AllowDrawing = true;
Service<DalamudInterface>.Get().SetCreditsDarkeningAnimation(false);
var configuration = Service<DalamudConfiguration>.Get();
if (this.chosenAutoUpdateBehavior.HasValue)
{
configuration.AutoUpdateBehavior = this.chosenAutoUpdateBehavior.Value;
}
configuration.SeenFtueLevels = this.currentFtueLevels;
configuration.QueueSave();
}
/// <inheritdoc/>
@ -144,10 +188,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 +254,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 +271,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,11 +288,31 @@ 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 DrawNextButton(State nextState)
void GoToNextState(State nextState)
{
this.isFadingOutForStateChange = true;
this.stateAfterFadeOut = nextState;
this.fadeOut.Restart();
}
bool DrawNextButton(State nextState)
{
// Draw big, centered next button at the bottom of the window
var buttonHeight = 30 * ImGuiHelpers.GlobalScale;
@ -249,11 +321,13 @@ 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);
return true;
}
return false;
}
switch (this.state)
@ -286,7 +360,66 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.apiBumpExplainerTexture.Value.ImGuiHandle,
this.apiBumpExplainerTexture.Value.Size);
DrawNextButton(State.Links);
if (!this.currentFtueLevels.TryGetValue(FtueLevels.AutoUpdate.Name, out var autoUpdateLevel) || autoUpdateLevel < FtueLevels.AutoUpdate.AutoUpdateInitial)
{
if (DrawNextButton(State.AskAutoUpdate))
{
this.currentFtueLevels[FtueLevels.AutoUpdate.Name] = FtueLevels.AutoUpdate.AutoUpdateInitial;
}
}
else
{
DrawNextButton(State.Links);
}
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 only notify you about updates while you are idle."));
ImGuiHelpers.ScaledDummy(15);
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.chosenAutoUpdateBehavior = 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.chosenAutoUpdateBehavior = AutoUpdateBehavior.OnlyNotify;
GoToNextState(State.Links);
}
}
break;
case State.Links:
@ -356,12 +489,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();
@ -384,4 +517,13 @@ internal sealed class ChangelogWindow : Window, IDisposable
public void Dispose()
{
}
private static class FtueLevels
{
public static class AutoUpdate
{
public const string Name = "AutoUpdate";
public const int AutoUpdateInitial = 1;
}
}
}

View file

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

View file

@ -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");

View file

@ -21,7 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings;
/// </summary>
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()
];
}
/// <summary>
@ -75,15 +85,6 @@ internal class SettingsWindow : Window
/// <inheritdoc/>
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),
};
}
}

View file

@ -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<AutoUpdatePreference> autoUpdatePreferences = [];
public override SettingsEntry[] Entries { get; } = Array.Empty<SettingsEntry>();
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 only notify you about updates while you are idle."));
ImGuiHelpers.ScaledDummy(8);
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior",
"When the game starts..."));
var behaviorInt = (int)this.behavior;
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 don't have auto-update rules for any plugins."));
}
ImGuiHelpers.ScaledDummy(2);
}
else
{
ImGuiHelpers.ScaledDummy(5);
var pic = Service<PluginImageCache>.Get();
var windowSize = ImGui.GetWindowSize();
var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale;
Guid? wantRemovePluginGuid = null;
foreach (var preference in this.autoUpdatePreferences)
{
var pmPlugin = Service<PluginManager>.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<AutoUpdatePreference.OptKind>())
{
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<DalamudConfiguration>.Get();
this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
this.checkPeriodically = configuration.CheckPeriodicallyForUpdates;
this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences;
base.Load();
}
public override void Save()
{
var configuration = Service<DalamudConfiguration>.Get();
configuration.AutoUpdateBehavior = this.behavior;
configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;
base.Save();
}
}

View file

@ -20,7 +20,7 @@ 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) =>
warning: v =>
{
// TODO: Maybe actually implement UI for the validity check...
if (v == XivChatType.None)
@ -62,12 +62,6 @@ public class SettingsTabGeneral : SettingsTab
c => c.PrintPluginsWelcomeMsg,
(v, c) => c.PrintPluginsWelcomeMsg = v),
new SettingsEntry<bool>(
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<bool>(
Loc.Localize("DalamudSettingsSystemMenu", "Dalamud buttons in system menu"),
Loc.Localize("DalamudSettingsSystemMenuMsgHint", "Add buttons for Dalamud plugins and settings to the system menu."),

View file

@ -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;
/// <summary>
@ -114,7 +112,7 @@ public sealed class DalamudPluginInterface : IDisposable
/// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session.
/// </summary>
public bool IsAutoUpdateComplete => Service<ChatHandlers>.Get().IsAutoUpdateComplete;
public bool IsAutoUpdateComplete => Service<AutoUpdateManager>.Get().IsAutoUpdateComplete;
/// <summary>
/// Gets the repository from which this plugin was installed.

View file

@ -0,0 +1,27 @@
namespace Dalamud.Plugin.Internal.AutoUpdate;
/// <summary>
/// Enum describing how plugins should be auto-updated at startup-.
/// </summary>
internal enum AutoUpdateBehavior
{
/// <summary>
/// Plugins should not be updated and the user should not be notified.
/// </summary>
None,
/// <summary>
/// The user should merely be notified about updates.
/// </summary>
OnlyNotify,
/// <summary>
/// Only plugins from the main repository should be updated.
/// </summary>
UpdateMainRepo,
/// <summary>
/// All plugins should be updated.
/// </summary>
UpdateAll,
}

View file

@ -0,0 +1,458 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CheapLoc;
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;
/// <summary>
/// Class to manage automatic updates for plugins.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class AutoUpdateManager : IServiceType
{
private static readonly ModuleLog Log = new("AUTOUPDATE");
/// <summary>
/// Time we should wait after login to update.
/// </summary>
private static readonly TimeSpan UpdateTimeAfterLogin = TimeSpan.FromSeconds(20);
/// <summary>
/// Time we should wait between scheduled update checks.
/// </summary>
private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(1.5);
/// <summary>
/// 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.
/// </summary>
private static readonly TimeSpan CooldownAfterUnblock = TimeSpan.FromSeconds(30);
[ServiceManager.ServiceDependency]
private readonly PluginManager pluginManager = Service<PluginManager>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration config = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly NotificationManager notificationManager = Service<NotificationManager>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudInterface dalamudInterface = Service<DalamudInterface>.Get();
private readonly IConsoleVariable<bool> isDryRun;
private DateTime? loginTime;
private DateTime? lastUpdateCheckTime;
private DateTime? unblockedSince;
private bool hasStartedInitialUpdateThisSession;
private IActiveNotification? updateNotification;
private Task? autoUpdateTask;
/// <summary>
/// Initializes a new instance of the <see cref="AutoUpdateManager"/> class.
/// </summary>
/// <param name="console">Console service.</param>
[ServiceManager.ServiceConstructor]
public AutoUpdateManager(ConsoleManager console)
{
Service<ClientState>.GetAsync().ContinueWith(
t =>
{
t.Result.Login += this.OnLogin;
t.Result.Logout += this.OnLogout;
});
Service<Framework>.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; });
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;
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,
}
/// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session.
/// </summary>
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<AvailablePluginUpdate> 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<AvailablePluginUpdate> updatablePlugins)
{
Log.Information("Found {UpdatablePluginsCount} plugins to update", updatablePlugins.Count);
if (updatablePlugins.Count == 0)
return;
var notification = this.GetBaseNotification(new Notification
{
Title = Locs.NotificationTitleUpdatingPlugins,
Content = Locs.NotificationContentPreparingToUpdate(updatablePlugins.Count),
Type = NotificationType.Info,
InitialDuration = TimeSpan.MaxValue,
ShowIndeterminateIfNoExpiry = false,
UserDismissable = false,
Progress = 0,
Icon = INotificationIcon.From(FontAwesomeIcon.Download),
Minimized = false,
});
var progress = new Progress<PluginManager.PluginUpdateProgress>();
progress.ProgressChanged += (_, progress) =>
{
notification.Content = Locs.NotificationContentUpdating(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);
notification.DrawActions += _ =>
{
ImGuiHelpers.ScaledDummy(2);
if (DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller))
{
Service<DalamudInterface>.Get().OpenPluginInstaller();
notification.DismissNow();
}
};
// Update the notification to show the final state
var pluginUpdateStatusEnumerable = pluginStates as PluginUpdateStatus[] ?? pluginStates.ToArray();
if (pluginUpdateStatusEnumerable.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 = Locs.NotificationTitleUpdatesSuccessful;
notification.MinimizedText = Locs.NotificationContentUpdatesSuccessfulMinimized;
notification.Type = NotificationType.Success;
notification.Content = Locs.NotificationContentUpdatesSuccessful;
}
else
{
notification.Title = Locs.NotificationTitleUpdatesFailed;
notification.MinimizedText = Locs.NotificationContentUpdatesFailedMinimized;
notification.Type = NotificationType.Error;
notification.Content = Locs.NotificationContentUpdatesFailed;
var failedPlugins = pluginUpdateStatusEnumerable
.Where(x => x.Status != PluginUpdateStatus.StatusKind.Success)
.Select(x => x.Name).ToList();
notification.Content += "\n" + Locs.NotificationContentFailedPlugins(failedPlugins);
}
}
private void NotifyUpdatesAreAvailable(ICollection<AvailablePluginUpdate> updatablePlugins)
{
if (updatablePlugins.Count == 0)
return;
var notification = this.GetBaseNotification(new Notification
{
Title = Locs.NotificationTitleUpdatesAvailable,
Content = Locs.NotificationContentUpdatesAvailable(updatablePlugins.Count),
MinimizedText = Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count),
Type = NotificationType.Info,
InitialDuration = TimeSpan.MaxValue,
ShowIndeterminateIfNoExpiry = false,
Icon = INotificationIcon.From(FontAwesomeIcon.Download),
});
notification.DrawActions += _ =>
{
ImGuiHelpers.ScaledDummy(2);
if (DalamudComponents.PrimaryButton(Locs.NotificationButtonUpdate))
{
this.KickOffAutoUpdates(updatablePlugins);
notification.DismissNow();
}
ImGui.SameLine();
if (DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller))
{
Service<DalamudInterface>.Get().OpenPluginInstaller();
notification.DismissNow();
}
};
}
private List<AvailablePluginUpdate> 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<Condition>.Get();
return this.IsPluginManagerReady() &&
!this.dalamudInterface.IsPluginInstallerOpen &&
condition.OnlyAny(ConditionFlag.NormalConditions,
ConditionFlag.Jumping,
ConditionFlag.Mounted,
ConditionFlag.UsingParasol);
}
private bool IsPluginManagerReady()
{
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<string> failedPlugins)
=> string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugins: {0}"), string.Join(", ", failedPlugins));
}
}

View file

@ -977,32 +977,39 @@ internal class PluginManager : IInternalDisposableService
/// <summary>
/// Update all non-dev plugins.
/// </summary>
/// <param name="ignoreDisabled">Ignore disabled plugins.</param>
/// <param name="toUpdate">List of plugins to update.</param>
/// <param name="dryRun">Perform a dry run, don't install anything.</param>
/// <param name="autoUpdate">If this action was performed as part of an auto-update.</param>
/// <param name="progress">An <see cref="IProgress{T}"/> implementation to receive progress updates about the installation status.</param>
/// <returns>Success or failure and a list of updated plugin metadata.</returns>
public async Task<IEnumerable<PluginUpdateStatus>> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun, bool autoUpdate = false)
public async Task<IEnumerable<PluginUpdateStatus>> UpdatePluginsAsync(
ICollection<AvailablePluginUpdate> toUpdate,
bool dryRun,
bool autoUpdate = false,
IProgress<PluginUpdateProgress>? progress = null)
{
Log.Information("Starting plugin update");
var updateTasks = new List<Task<PluginUpdateStatus>>();
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<PluginUpdateStatus> 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;
}
}
/// <summary>
@ -1832,6 +1856,11 @@ internal class PluginManager : IInternalDisposableService
}
}
/// <summary>
/// Class representing progress of an update operation.
/// </summary>
public record PluginUpdateProgress(int PluginsProcessed, int TotalPlugins, IPluginManifest CurrentPluginManifest);
/// <summary>
/// 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.

View file

@ -1,4 +1,6 @@
using Dalamud.Game.ClientState.Conditions;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Conditions;
namespace Dalamud.Plugin.Services;
@ -51,4 +53,26 @@ public interface ICondition
/// <returns>Whether any single provided flag is set.</returns>
/// <param name="flags">The condition flags to check.</param>
public bool Any(params ConditionFlag[] flags);
/// <summary>
/// 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.
/// </summary>
/// <param name="other">The array of flags to check.</param>
/// <returns>Returns a bool.</returns>
public bool OnlyAny(params ConditionFlag[] other);
/// <summary>
/// Check that *only* the specified flags are set. Unlike <see cref="OnlyAny"/>, this method requires that all the
/// specified flags are set and no others are present.
/// </summary>
/// <param name="other">The array of flags to check.</param>
/// <returns>Returns a bool.</returns>
public bool OnlyAll(params ConditionFlag[] other);
/// <summary>
/// Convert the conditions array to a set of all set condition flags.
/// </summary>
/// <returns>Returns a set.</returns>
public IReadOnlySet<ConditionFlag> AsReadOnlySet();
}