Merge pull request #1832 from goatcorp/apiX-rollup

[apiX] Rollup changes from master
This commit is contained in:
goat 2024-06-16 13:01:50 +02:00 committed by GitHub
commit 80555d92ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1526 additions and 311 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>
@ -228,6 +228,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public int LogLinesLimit { get; set; } = 10000;
/// <summary>
/// Gets or sets a list representing the command history for the Dalamud Console.
/// </summary>
public List<string> LogCommandHistory { get; set; } = new();
/// <summary>
/// Gets or sets a value indicating whether or not the dev bar should open at startup.
/// </summary>
@ -429,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.
@ -464,6 +474,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>
@ -549,6 +569,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
private void SetDefaults()
{
#pragma warning disable CS0618
// "Reduced motion"
if (!this.ReduceMotions.HasValue)
{
@ -570,6 +591,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

@ -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
/// <param name="defaultValue">The default value to use if none is specified.</param>
/// <returns>An <see cref="ArgumentInfo"/> instance.</returns>
/// <exception cref="ArgumentException">Thrown if the given type cannot be handled by the console system.</exception>
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<ArgumentInfo> { TypeToArgument(typeof(T)) };
this.ValidArguments = new List<ArgumentInfo> { TypeToArgument(typeof(T), null) };
}
/// <inheritdoc/>
@ -500,7 +499,20 @@ internal partial class ConsoleManager : IServiceType
public override bool Invoke(IEnumerable<object> 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;

View file

@ -21,7 +21,8 @@ namespace Dalamud.Console;
#pragma warning restore SA1015
public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
{
private readonly ConsoleManager console;
[ServiceManager.ServiceDependency]
private readonly ConsoleManager console = Service<ConsoleManager>.Get();
private readonly List<IConsoleEntry> trackedEntries = new();
@ -29,12 +30,9 @@ 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, ConsoleManager console)
internal ConsoleManagerPluginScoped(LocalPlugin plugin)
{
this.console = console;
this.Prefix = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(plugin.InternalName);
}

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>9.1.0.10</DalamudVersion>
<DalamudVersion>9.1.0.12</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>

View file

@ -29,40 +29,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(
@ -105,8 +71,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();
@ -114,19 +78,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>
@ -175,9 +132,6 @@ internal class ChatHandlers : IServiceType
{
if (!this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
if (!this.startedAutoUpdatingPlugins)
this.AutoUpdatePluginsWithRetry();
}
// For injections while logged in
@ -272,89 +226,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,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;
@ -122,6 +124,27 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
/// Gets client state address resolver.
/// </summary>
internal ClientStateAddressResolver AddressResolver => this.address;
/// <inheritdoc/>
public bool IsClientIdle(out ConditionFlag blockingFlag)
{
blockingFlag = 0;
if (this.LocalPlayer is null) return true;
var condition = Service<Conditions.Condition>.GetNullable();
var blockingConditions = condition.AsReadOnlySet().Except([
ConditionFlag.NormalConditions,
ConditionFlag.Jumping,
ConditionFlag.Mounted,
ConditionFlag.UsingParasol]);
blockingFlag = blockingConditions.FirstOrDefault();
return blockingFlag == 0;
}
/// <inheritdoc/>
public bool IsClientIdle() => this.IsClientIdle(out _);
/// <summary>
/// Dispose of managed and unmanaged resources.
@ -269,6 +292,12 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
/// <inheritdoc/>
public bool IsGPosing => this.clientStateService.IsGPosing;
/// <inheritdoc/>
public bool IsClientIdle(out ConditionFlag blockingFlag) => this.clientStateService.IsClientIdle(out blockingFlag);
/// <inheritdoc/>
public bool IsClientIdle() => this.clientStateService.IsClientIdle();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{

View file

@ -1,3 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
@ -67,6 +70,22 @@ internal sealed class Condition : IInternalDisposableService, ICondition
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.Dispose(true);
/// <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;
}
/// <inheritdoc/>
public bool Any()
@ -96,6 +115,25 @@ internal sealed class Condition : IInternalDisposableService, ICondition
return false;
}
/// <inheritdoc/>
public bool AnyExcept(params ConditionFlag[] excluded)
{
return !this.AsReadOnlySet().Intersect(excluded).Any();
}
/// <inheritdoc/>
public bool OnlyAny(params ConditionFlag[] other)
{
return !this.AsReadOnlySet().Except(other).Any();
}
/// <inheritdoc/>
public bool EqualTo(params ConditionFlag[] other)
{
var resultSet = this.AsReadOnlySet();
return resultSet.SetEquals(other);
}
private void Dispose(bool disposing)
{
@ -173,6 +211,9 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition
this.ConditionChange = null;
}
/// <inheritdoc/>
public IReadOnlySet<ConditionFlag> AsReadOnlySet() => this.conditionService.AsReadOnlySet();
/// <inheritdoc/>
public bool Any() => this.conditionService.Any();
@ -180,5 +221,14 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition
/// <inheritdoc/>
public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags);
/// <inheritdoc/>
public bool AnyExcept(params ConditionFlag[] except) => this.conditionService.AnyExcept(except);
/// <inheritdoc/>
public bool OnlyAny(params ConditionFlag[] other) => this.conditionService.OnlyAny(other);
/// <inheritdoc/>
public bool EqualTo(params ConditionFlag[] other) => this.conditionService.EqualTo(other);
private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value);
}

View file

@ -495,6 +495,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;

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

@ -37,6 +37,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;
@ -44,9 +45,10 @@ internal class ConsoleWindow : Window, IDisposable
// Fields below should be touched only from the main thread.
private readonly RollingList<LogEntry> logText;
private readonly RollingList<LogEntry> filteredLogEntries;
private readonly List<string> history = new();
private readonly List<PluginFilterEntry> pluginFilters = new();
private readonly DalamudConfiguration configuration;
private int newRolledLines;
private bool pendingRefilter;
@ -78,6 +80,9 @@ internal class ConsoleWindow : Window, IDisposable
private int historyPos;
private int copyStart = -1;
private string? completionZipText = null;
private int completionTabIdx = 0;
private IActiveNotification? prevCopyNotification;
/// <summary>Initializes a new instance of the <see cref="ConsoleWindow"/> class.</summary>
@ -85,6 +90,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;
@ -111,7 +118,7 @@ internal class ConsoleWindow : Window, IDisposable
this.logText = new(limit);
this.filteredLogEntries = new(limit);
configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved;
this.configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved;
unsafe
{
@ -130,7 +137,7 @@ internal class ConsoleWindow : Window, IDisposable
public void Dispose()
{
SerilogEventSink.Instance.LogLine -= this.OnLogLine;
Service<DalamudConfiguration>.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
this.configuration.DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
if (Service<Framework>.GetNullable() is { } framework)
framework.Update -= this.FrameworkOnUpdate;
@ -314,9 +321,10 @@ internal class ConsoleWindow : Window, IDisposable
ref this.commandText,
255,
ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion |
ImGuiInputTextFlags.CallbackHistory,
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;
}
@ -460,8 +468,6 @@ internal class ConsoleWindow : Window, IDisposable
private void DrawOptionsToolbar()
{
var configuration = Service<DalamudConfiguration>.Get();
ImGui.PushItemWidth(150.0f * ImGuiHelpers.GlobalScale);
if (ImGui.BeginCombo("##log_level", $"{EntryPoint.LogLevelSwitch.MinimumLevel}+"))
{
@ -470,8 +476,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();
}
}
@ -484,13 +490,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;
@ -638,18 +644,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");
@ -658,8 +664,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();
}
@ -795,23 +801,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<ConsoleManager>.Get().ProcessCommand(this.commandText);
this.commandText = string.Empty;
@ -831,6 +832,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);
@ -841,22 +847,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<ConsoleManager>.Get().Entries
.Where(x => x.Key.StartsWith(words[0]))
.Where(x => x.Key.StartsWith(wordToComplete))
.Select(x => x.Key);
candidates = candidates.Union(
Service<CommandManager>.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;
@ -867,7 +898,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--;
}
@ -875,7 +906,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;
}
@ -884,7 +915,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);

View file

@ -208,6 +208,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 ||
@ -712,8 +725,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;
@ -1807,22 +1824,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)
{
@ -1854,7 +1911,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;
@ -1868,7 +1925,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);
@ -1886,11 +1943,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);
@ -1934,7 +1991,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);
@ -1954,7 +2011,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);
@ -1964,12 +2021,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;
@ -1987,7 +2044,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;
@ -2031,7 +2088,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))
{
@ -2152,11 +2209,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);
@ -2420,7 +2488,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);

View file

@ -12,7 +12,6 @@ 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 +299,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 +579,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,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.
/// </summary>
public bool IsGPosing { get; }
/// <summary>
/// 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.
/// </summary>
/// <param name="blockingFlag">An outvar containing the first observed condition blocking the "idle" state. 0 if idle.</param>
/// <returns>Returns true if the client is idle, false otherwise.</returns>
public bool IsClientIdle(out ConditionFlag blockingFlag);
/// <summary>
/// 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.
/// </summary>
/// <returns>Returns true if the client is idle, false otherwise.</returns>
public bool IsClientIdle() => this.IsClientIdle(out _);
}

View file

@ -1,4 +1,6 @@
using Dalamud.Game.ClientState.Conditions;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Conditions;
namespace Dalamud.Plugin.Services;
@ -38,6 +40,12 @@ public interface ICondition
/// <inheritdoc cref="this[int]"/>
public bool this[ConditionFlag flag] => this[(int)flag];
/// <summary>
/// Convert the conditions array to a set of all set condition flags.
/// </summary>
/// <returns>Returns a set.</returns>
public IReadOnlySet<ConditionFlag> AsReadOnlySet();
/// <summary>
/// Check if any condition flags are set.
@ -51,4 +59,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 the specified condition flags are *not* present in the current conditions.
/// </summary>
/// <param name="except">The array of flags to check.</param>
/// <returns>Returns false if any of the listed conditions are present, true otherwise.</returns>
public bool AnyExcept(params ConditionFlag[] except);
/// <summary>
/// Check that *only* any of the condition flags specified are set.
/// </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 EqualTo(params ConditionFlag[] other);
}