diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0a6b44eeb..d78c87d68 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -7,6 +7,10 @@ concurrency:
jobs:
build:
name: Build on Windows
+ permissions:
+ id-token: write
+ contents: read
+ attestations: write
runs-on: windows-2022
steps:
- name: Checkout Dalamud
@@ -40,6 +44,17 @@ jobs:
run: .\sign.ps1 .\bin\Release
- name: Create hashlist
run: .\CreateHashList.ps1 .\bin\Release
+ - name: Attest Build
+ if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }}
+ uses: actions/attest-build-provenance@v1
+ with:
+ subject-path: |
+ bin/Release/hashes.json
+ bin/Release/Dalamud.dll
+ bin/Release/DalamudCrashHandler.exe
+ bin/Release/Dalamud.*.dll
+ bin/Release/Dalamud.*.exe
+ bin/Release/FFXIVClientStructs.dll
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
diff --git a/Dalamud/Interface/DalamudWindowOpenKinds.cs b/Dalamud/Interface/DalamudWindowOpenKinds.cs
index 1f82cca49..588ff858b 100644
--- a/Dalamud/Interface/DalamudWindowOpenKinds.cs
+++ b/Dalamud/Interface/DalamudWindowOpenKinds.cs
@@ -14,6 +14,11 @@ public enum PluginInstallerOpenKind
/// Open to the "Installed Plugins" page.
///
InstalledPlugins,
+
+ ///
+ /// Open to the "Can be updated" page.
+ ///
+ UpdateablePlugins,
///
/// Open to the "Changelogs" page.
diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs
index 18936687a..cc6f1b64c 100644
--- a/Dalamud/Interface/Internal/DalamudCommands.cs
+++ b/Dalamud/Interface/Internal/DalamudCommands.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
+using System.IO;
using System.Linq;
using CheapLoc;
@@ -143,6 +144,13 @@ internal class DalamudCommands : IServiceType
HelpMessage = "ImGui DEBUG",
ShowInHelp = false,
});
+
+ commandManager.AddHandler("/xlcopylog", new CommandInfo(this.OnCopyLogCommand)
+ {
+ HelpMessage = Loc.Localize(
+ "DalamudCopyLogHelp",
+ "Copy the dalamud.log file to your clipboard."),
+ });
}
private void OnUnloadCommand(string command, string arguments)
@@ -406,4 +414,17 @@ internal class DalamudCommands : IServiceType
{
Service.Get().ToggleProfilerWindow();
}
+
+ private void OnCopyLogCommand(string command, string arguments)
+ {
+ var chatGui = Service.Get();
+ var logPath = Path.Join(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "XIVLauncher",
+ "dalamud.log");
+ var message = Util.CopyFilesToClipboard([logPath])
+ ? Loc.Localize("DalamudLogCopySuccess", "Log file copied to clipboard.")
+ : Loc.Localize("DalamudLogCopyFailure", "Could not copy log file to clipboard.");
+ chatGui.Print(message);
+ }
}
diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs
index 3f94b2d2e..ec2a1c15b 100644
--- a/Dalamud/Interface/Internal/PluginCategoryManager.cs
+++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs
@@ -16,46 +16,49 @@ internal class PluginCategoryManager
///
/// First categoryId for tag based categories.
///
- public const int FirstTagBasedCategoryId = 100;
+ private const int FirstTagBasedCategoryId = 100;
private readonly CategoryInfo[] categoryList =
- {
- new(0, "special.all", () => Locs.Category_All),
- new(1, "special.isTesting", () => Locs.Category_IsTesting, CategoryInfo.AppearCondition.DoPluginTest),
- new(2, "special.availableForTesting", () => Locs.Category_AvailableForTesting, CategoryInfo.AppearCondition.DoPluginTest),
- new(10, "special.devInstalled", () => Locs.Category_DevInstalled),
- new(11, "special.devIconTester", () => Locs.Category_IconTester),
- new(12, "special.dalamud", () => Locs.Category_Dalamud),
- new(13, "special.plugins", () => Locs.Category_Plugins),
- new(14, "special.profiles", () => Locs.Category_PluginProfiles),
- new(FirstTagBasedCategoryId + 0, "other", () => Locs.Category_Other),
- new(FirstTagBasedCategoryId + 1, "jobs", () => Locs.Category_Jobs),
- new(FirstTagBasedCategoryId + 2, "ui", () => Locs.Category_UI),
- new(FirstTagBasedCategoryId + 3, "minigames", () => Locs.Category_MiniGames),
- new(FirstTagBasedCategoryId + 4, "inventory", () => Locs.Category_Inventory),
- new(FirstTagBasedCategoryId + 5, "sound", () => Locs.Category_Sound),
- new(FirstTagBasedCategoryId + 6, "social", () => Locs.Category_Social),
- new(FirstTagBasedCategoryId + 7, "utility", () => Locs.Category_Utility),
+ [
+ new(CategoryKind.All, "special.all", () => Locs.Category_All),
+ new(CategoryKind.IsTesting, "special.isTesting", () => Locs.Category_IsTesting, CategoryInfo.AppearCondition.DoPluginTest),
+ new(CategoryKind.AvailableForTesting, "special.availableForTesting", () => Locs.Category_AvailableForTesting, CategoryInfo.AppearCondition.DoPluginTest),
+ new(CategoryKind.DevInstalled, "special.devInstalled", () => Locs.Category_DevInstalled),
+ new(CategoryKind.IconTester, "special.devIconTester", () => Locs.Category_IconTester),
+ new(CategoryKind.DalamudChangelogs, "special.dalamud", () => Locs.Category_Dalamud),
+ new(CategoryKind.PluginChangelogs, "special.plugins", () => Locs.Category_Plugins),
+ new(CategoryKind.PluginProfiles, "special.profiles", () => Locs.Category_PluginProfiles),
+ new(CategoryKind.UpdateablePlugins, "special.updateable", () => Locs.Category_UpdateablePlugins),
+
+ // Tag-driven categories
+ new(CategoryKind.Other, "other", () => Locs.Category_Other),
+ new(CategoryKind.Jobs, "jobs", () => Locs.Category_Jobs),
+ new(CategoryKind.Ui, "ui", () => Locs.Category_UI),
+ new(CategoryKind.MiniGames, "minigames", () => Locs.Category_MiniGames),
+ new(CategoryKind.Inventory, "inventory", () => Locs.Category_Inventory),
+ new(CategoryKind.Sound, "sound", () => Locs.Category_Sound),
+ new(CategoryKind.Social, "social", () => Locs.Category_Social),
+ new(CategoryKind.Utility, "utility", () => Locs.Category_Utility)
// order doesn't matter, all tag driven categories should have Id >= FirstTagBasedCategoryId
- };
+ ];
private GroupInfo[] groupList =
- {
- new(GroupKind.DevTools, () => Locs.Group_DevTools, 10, 11),
- new(GroupKind.Installed, () => Locs.Group_Installed, 0, 1, 14),
- new(GroupKind.Available, () => Locs.Group_Available, 0),
- new(GroupKind.Changelog, () => Locs.Group_Changelog, 0, 12, 13),
+ [
+ new(GroupKind.DevTools, () => Locs.Group_DevTools, CategoryKind.DevInstalled, CategoryKind.IconTester),
+ new(GroupKind.Installed, () => Locs.Group_Installed, CategoryKind.All, CategoryKind.IsTesting, CategoryKind.UpdateablePlugins, CategoryKind.PluginProfiles),
+ new(GroupKind.Available, () => Locs.Group_Available, CategoryKind.All),
+ new(GroupKind.Changelog, () => Locs.Group_Changelog, CategoryKind.All, CategoryKind.DalamudChangelogs, CategoryKind.PluginChangelogs)
// order important, used for drawing, keep in sync with defaults for currentGroupIdx
- };
+ ];
private int currentGroupIdx = 2;
- private int currentCategoryIdx = 0;
+ private CategoryKind currentCategoryKind = CategoryKind.All;
private bool isContentDirty;
- private Dictionary mapPluginCategories = new();
- private List highlightedCategoryIds = new();
+ private Dictionary mapPluginCategories = new();
+ private List highlightedCategoryKinds = new();
///
/// Type of category group.
@@ -83,6 +86,97 @@ internal class PluginCategoryManager
Changelog,
}
+ ///
+ /// Type of category.
+ ///
+ public enum CategoryKind
+ {
+ ///
+ /// All plugins.
+ ///
+ All = 0,
+
+ ///
+ /// Plugins currently being tested.
+ ///
+ IsTesting = 1,
+
+ ///
+ /// Plugins available for testing.
+ ///
+ AvailableForTesting = 2,
+
+ ///
+ /// Installed dev plugins.
+ ///
+ DevInstalled = 10,
+
+ ///
+ /// Icon tester.
+ ///
+ IconTester = 11,
+
+ ///
+ /// Changelogs for Dalamud.
+ ///
+ DalamudChangelogs = 12,
+
+ ///
+ /// Changelogs for plugins.
+ ///
+ PluginChangelogs = 13,
+
+ ///
+ /// Change plugin profiles.
+ ///
+ PluginProfiles = 14,
+
+ ///
+ /// Updateable plugins.
+ ///
+ UpdateablePlugins = 15,
+
+ ///
+ /// Plugins tagged as "other".
+ ///
+ Other = FirstTagBasedCategoryId + 0,
+
+ ///
+ /// Plugins tagged as "jobs".
+ ///
+ Jobs = FirstTagBasedCategoryId + 1,
+
+ ///
+ /// Plugins tagged as "ui".
+ ///
+ Ui = FirstTagBasedCategoryId + 2,
+
+ ///
+ /// Plugins tagged as "minigames".
+ ///
+ MiniGames = FirstTagBasedCategoryId + 3,
+
+ ///
+ /// Plugins tagged as "inventory".
+ ///
+ Inventory = FirstTagBasedCategoryId + 4,
+
+ ///
+ /// Plugins tagged as "sound".
+ ///
+ Sound = FirstTagBasedCategoryId + 5,
+
+ ///
+ /// Plugins tagged as "social".
+ ///
+ Social = FirstTagBasedCategoryId + 6,
+
+ ///
+ /// Plugins tagged as "utility".
+ ///
+ Utility = FirstTagBasedCategoryId + 7,
+ }
+
///
/// Gets the list of all known categories.
///
@@ -92,39 +186,50 @@ internal class PluginCategoryManager
/// Gets the list of all known UI groups.
///
public GroupInfo[] GroupList => this.groupList;
-
+
///
- /// Gets or sets current group.
+ /// Gets or sets the current group kind.
///
- public int CurrentGroupIdx
+ public GroupKind CurrentGroupKind
{
- get => this.currentGroupIdx;
+ get => this.groupList[this.currentGroupIdx].GroupKind;
set
{
- if (this.currentGroupIdx != value)
+ var newIdx = Array.FindIndex(this.groupList, x => x.GroupKind == value);
+ if (newIdx >= 0)
{
- this.currentGroupIdx = value;
- this.currentCategoryIdx = 0;
+ this.currentGroupIdx = newIdx;
+ this.currentCategoryKind = this.CurrentGroup.Categories.First();
this.isContentDirty = true;
}
}
}
-
+
///
- /// Gets or sets current category, index in current Group.Categories array.
+ /// Gets information about currently selected group.
///
- public int CurrentCategoryIdx
+ public GroupInfo CurrentGroup => this.groupList[this.currentGroupIdx];
+
+ ///
+ /// Gets or sets the current category kind.
+ ///
+ public CategoryKind CurrentCategoryKind
{
- get => this.currentCategoryIdx;
+ get => this.currentCategoryKind;
set
{
- if (this.currentCategoryIdx != value)
+ if (this.currentCategoryKind != value)
{
- this.currentCategoryIdx = value;
+ this.currentCategoryKind = value;
this.isContentDirty = true;
}
}
}
+
+ ///
+ /// Gets information about currently selected category.
+ ///
+ public CategoryInfo CurrentCategory => this.categoryList.First(x => x.CategoryKind == this.currentCategoryKind);
///
/// Gets a value indicating whether current group + category selection changed recently.
@@ -133,13 +238,12 @@ internal class PluginCategoryManager
public bool IsContentDirty => this.isContentDirty;
///
- /// Gets a value indicating whether and are valid.
+ /// Gets a value indicating whether and are valid.
///
public bool IsSelectionValid =>
(this.currentGroupIdx >= 0) &&
(this.currentGroupIdx < this.groupList.Length) &&
- (this.currentCategoryIdx >= 0) &&
- (this.currentCategoryIdx < this.groupList[this.currentGroupIdx].Categories.Count);
+ this.groupList[this.currentGroupIdx].Categories.Contains(this.currentCategoryKind);
///
/// Rebuild available categories based on currently available plugins.
@@ -151,10 +255,10 @@ internal class PluginCategoryManager
this.mapPluginCategories.Clear();
var groupAvail = Array.Find(this.groupList, x => x.GroupKind == GroupKind.Available);
- var prevCategoryIds = new List();
+ var prevCategoryIds = new List();
prevCategoryIds.AddRange(groupAvail.Categories);
- var categoryList = new List();
+ var categoryList = new List();
var allCategoryIndices = new List();
foreach (var manifest in availablePlugins)
@@ -170,10 +274,10 @@ internal class PluginCategoryManager
var matchIdx = Array.FindIndex(this.CategoryList, x => x.Tag.Equals(tag, StringComparison.InvariantCultureIgnoreCase));
if (matchIdx >= 0)
{
- var categoryId = this.CategoryList[matchIdx].CategoryId;
- if (categoryId >= FirstTagBasedCategoryId)
+ var categoryKind = this.CategoryList[matchIdx].CategoryKind;
+ if ((int)categoryKind >= FirstTagBasedCategoryId)
{
- categoryList.Add(categoryId);
+ categoryList.Add(categoryKind);
if (!allCategoryIndices.Contains(matchIdx))
{
@@ -185,7 +289,7 @@ internal class PluginCategoryManager
}
if (PluginManager.HasTestingVersion(manifest) || manifest.IsTestingExclusive)
- categoryList.Add(2);
+ categoryList.Add(CategoryKind.AvailableForTesting);
// always add, even if empty
this.mapPluginCategories.Add(manifest, categoryList.ToArray());
@@ -203,11 +307,11 @@ internal class PluginCategoryManager
foreach (var categoryIdx in allCategoryIndices)
{
- groupAvail.Categories.Add(this.CategoryList[categoryIdx].CategoryId);
+ groupAvail.Categories.Add(this.CategoryList[categoryIdx].CategoryKind);
}
// compare with prev state and mark as dirty if needed
- var noCategoryChanges = Enumerable.SequenceEqual(prevCategoryIds, groupAvail.Categories);
+ var noCategoryChanges = prevCategoryIds.SequenceEqual(groupAvail.Categories);
if (!noCategoryChanges)
{
this.isContentDirty = true;
@@ -228,20 +332,20 @@ internal class PluginCategoryManager
{
var groupInfo = this.groupList[this.currentGroupIdx];
- var includeAll = (this.currentCategoryIdx == 0) || (groupInfo.GroupKind != GroupKind.Available);
+ var includeAll = (this.currentCategoryKind == CategoryKind.All) || (groupInfo.GroupKind != GroupKind.Available);
if (includeAll)
{
result.AddRange(plugins);
}
else
{
- var selectedCategoryInfo = Array.Find(this.categoryList, x => x.CategoryId == groupInfo.Categories[this.currentCategoryIdx]);
+ var selectedCategoryInfo = Array.Find(this.categoryList, x => x.CategoryKind == this.currentCategoryKind);
foreach (var plugin in plugins)
{
if (this.mapPluginCategories.TryGetValue(plugin, out var pluginCategoryIds))
{
- var matchIdx = Array.IndexOf(pluginCategoryIds, selectedCategoryInfo.CategoryId);
+ var matchIdx = Array.IndexOf(pluginCategoryIds, selectedCategoryInfo.CategoryKind);
if (matchIdx >= 0)
{
result.Add(plugin);
@@ -269,20 +373,19 @@ internal class PluginCategoryManager
/// List of plugins whose categories should be highlighted.
public void SetCategoryHighlightsForPlugins(IEnumerable plugins)
{
- this.highlightedCategoryIds.Clear();
+ ArgumentNullException.ThrowIfNull(plugins);
+
+ this.highlightedCategoryKinds.Clear();
- if (plugins != null)
+ foreach (var entry in plugins)
{
- foreach (var entry in plugins)
+ if (this.mapPluginCategories.TryGetValue(entry, out var pluginCategories))
{
- if (this.mapPluginCategories.TryGetValue(entry, out var pluginCategories))
+ foreach (var categoryKind in pluginCategories)
{
- foreach (var categoryId in pluginCategories)
+ if (!this.highlightedCategoryKinds.Contains(categoryKind))
{
- if (!this.highlightedCategoryIds.Contains(categoryId))
- {
- this.highlightedCategoryIds.Add(categoryId);
- }
+ this.highlightedCategoryKinds.Add(categoryKind);
}
}
}
@@ -292,9 +395,9 @@ internal class PluginCategoryManager
///
/// Checks if category should be highlighted.
///
- /// CategoryId to check.
+ /// CategoryKind to check.
/// true if highlight is needed.
- public bool IsCategoryHighlighted(int categoryId) => this.highlightedCategoryIds.Contains(categoryId);
+ public bool IsCategoryHighlighted(CategoryKind categoryKind) => this.highlightedCategoryKinds.Contains(categoryKind);
private IEnumerable GetCategoryTagsForManifest(PluginManifest pluginManifest)
{
@@ -314,7 +417,7 @@ internal class PluginCategoryManager
///
/// Unique Id number of category, tag match based should be greater of equal .
///
- public int CategoryId;
+ public CategoryKind CategoryKind;
///
/// Tag from plugin manifest to match.
@@ -326,13 +429,13 @@ internal class PluginCategoryManager
///
/// Initializes a new instance of the struct.
///
- /// Unique id of category.
+ /// Kind of the category.
/// Tag to match.
/// Function returning localized name of category.
/// Condition to be checked when deciding whether this category should be shown.
- public CategoryInfo(int categoryId, string tag, Func nameFunc, AppearCondition condition = AppearCondition.None)
+ public CategoryInfo(CategoryKind categoryKind, string tag, Func nameFunc, AppearCondition condition = AppearCondition.None)
{
- this.CategoryId = categoryId;
+ this.CategoryKind = categoryKind;
this.Tag = tag;
this.nameFunc = nameFunc;
this.Condition = condition;
@@ -378,7 +481,7 @@ internal class PluginCategoryManager
///
/// List of categories in container.
///
- public List Categories;
+ public List Categories;
private Func nameFunc;
@@ -388,7 +491,7 @@ internal class PluginCategoryManager
/// Type of group.
/// Function returning localized name of category.
/// List of category Ids to hardcode.
- public GroupInfo(GroupKind groupKind, Func nameFunc, params int[] categories)
+ public GroupInfo(GroupKind groupKind, Func nameFunc, params CategoryKind[] categories)
{
this.GroupKind = groupKind;
this.nameFunc = nameFunc;
@@ -432,6 +535,8 @@ internal class PluginCategoryManager
public static string Category_PluginProfiles => Loc.Localize("InstallerCategoryPluginProfiles", "Plugin Collections");
+ public static string Category_UpdateablePlugins => Loc.Localize("InstallerCategoryCanBeUpdated", "Can be updated");
+
public static string Category_Other => Loc.Localize("InstallerCategoryOther", "Other");
public static string Category_Jobs => Loc.Localize("InstallerCategoryJobs", "Jobs");
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index 80e0a0c35..c177f2933 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -142,7 +142,7 @@ internal class PluginInstallerWindow : Window, IDisposable
public PluginInstallerWindow(PluginImageCache imageCache, DalamudConfiguration configuration)
: base(
Locs.WindowTitle + (configuration.DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller",
- ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar)
+ ImGuiWindowFlags.NoScrollbar)
{
this.IsOpen = true;
this.imageCache = imageCache;
@@ -222,6 +222,14 @@ internal class PluginInstallerWindow : Window, IDisposable
IsTesting = 1 << 6,
}
+ private enum InstalledPluginListFilter
+ {
+ None,
+ Testing,
+ Updateable,
+ Dev,
+ }
+
private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress ||
this.updateStatus == OperationStatus.InProgress ||
this.enableDisableStatus == OperationStatus.InProgress;
@@ -421,21 +429,27 @@ internal class PluginInstallerWindow : Window, IDisposable
{
case PluginInstallerOpenKind.AllPlugins:
// Plugins group
- this.categoryManager.CurrentGroupIdx = 2;
+ this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Available;
// All category
- this.categoryManager.CurrentCategoryIdx = 0;
+ this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
break;
case PluginInstallerOpenKind.InstalledPlugins:
// Installed group
- this.categoryManager.CurrentGroupIdx = 1;
+ this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Installed;
// All category
- this.categoryManager.CurrentCategoryIdx = 0;
+ this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
+ break;
+ case PluginInstallerOpenKind.UpdateablePlugins:
+ // Installed group
+ this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Installed;
+ // Updateable category
+ this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.UpdateablePlugins;
break;
case PluginInstallerOpenKind.Changelogs:
// Changelog group
- this.categoryManager.CurrentGroupIdx = 3;
+ this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Changelog;
// Plugins category
- this.categoryManager.CurrentCategoryIdx = 2;
+ this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
@@ -598,7 +612,8 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - (style.ItemSpacing.X * 2) - searchInputWidth - searchClearButtonWidth);
var isProfileManager =
- this.categoryManager.CurrentGroupIdx == 1 && this.categoryManager.CurrentCategoryIdx == 2;
+ this.categoryManager.CurrentGroupKind == PluginCategoryManager.GroupKind.Installed &&
+ this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.PluginProfiles;
// Disable search if profile editor
using (ImRaii.Disabled(isProfileManager))
@@ -628,7 +643,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
// Disable sort if changelogs or profile editor
- using (ImRaii.Disabled(this.categoryManager.CurrentGroupIdx == 3 || isProfileManager))
+ using (ImRaii.Disabled(this.categoryManager.CurrentGroupKind == PluginCategoryManager.GroupKind.Changelog || isProfileManager))
{
ImGui.SameLine();
ImGui.SetCursorPosY(downShift);
@@ -727,7 +742,7 @@ internal class PluginInstallerWindow : Window, IDisposable
this.loadingIndicatorKind = LoadingIndicatorKind.UpdatingAll;
var toUpdate = this.pluginListUpdatable
- .Where(x => x.InstalledPlugin.IsLoaded)
+ .Where(x => x.InstalledPlugin.IsWantedByAnyProfile)
.ToList();
Task.Run(() => pluginManager.UpdatePluginsAsync(toUpdate, false))
@@ -768,9 +783,7 @@ internal class PluginInstallerWindow : Window, IDisposable
Service.Get().PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox);
notifications.AddNotification(Locs.Notifications_UpdatesInstalled(this.updatePluginCount), Locs.Notifications_UpdatesInstalledTitle, NotificationType.Success);
- var installedGroupIdx = this.categoryManager.GroupList.TakeWhile(
- x => x.GroupKind != PluginCategoryManager.GroupKind.Installed).Count();
- this.categoryManager.CurrentGroupIdx = installedGroupIdx;
+ this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Installed;
}
else if (this.updatePluginCount == 0)
{
@@ -1216,7 +1229,8 @@ internal class PluginInstallerWindow : Window, IDisposable
if (proxy.LocalPlugin != null)
{
- this.DrawInstalledPlugin(proxy.LocalPlugin, i++, proxy.RemoteManifest, true);
+ var update = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == proxy.LocalPlugin);
+ this.DrawInstalledPlugin(proxy.LocalPlugin, i++, proxy.RemoteManifest, update);
}
else if (proxy.RemoteManifest != null)
{
@@ -1226,8 +1240,8 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PopID();
}
}
-
- private void DrawInstalledPluginList(bool filterTesting)
+
+ private void DrawInstalledPluginList(InstalledPluginListFilter filter)
{
var pluginList = this.pluginListInstalled;
var manager = Service.Get();
@@ -1248,47 +1262,57 @@ internal class PluginInstallerWindow : Window, IDisposable
return;
}
+ var drewAny = false;
var i = 0;
foreach (var plugin in filteredList)
{
- if (filterTesting && !manager.HasTestingOptIn(plugin.Manifest))
+ if (filter == InstalledPluginListFilter.Testing && !manager.HasTestingOptIn(plugin.Manifest))
continue;
+
+ // Find applicable update and manifest, if we have them
+ AvailablePluginUpdate? update = null;
+ RemotePluginManifest? remoteManifest = null;
- // Find the applicable remote manifest
- var remoteManifest = this.pluginListAvailable
- .FirstOrDefault(rm => rm.InternalName == plugin.Manifest.InternalName &&
- rm.RepoUrl == plugin.Manifest.RepoUrl);
+ if (filter != InstalledPluginListFilter.Dev)
+ {
+ update = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin);
+ if (filter == InstalledPluginListFilter.Updateable && update == null)
+ continue;
- this.DrawInstalledPlugin(plugin, i++, remoteManifest);
+ // Find the applicable remote manifest
+ remoteManifest = this.pluginListAvailable
+ .FirstOrDefault(rm => rm.InternalName == plugin.Manifest.InternalName &&
+ rm.RepoUrl == plugin.Manifest.RepoUrl);
+ }
+ else if (!plugin.IsDev)
+ {
+ continue;
+ }
+
+ this.DrawInstalledPlugin(plugin, i++, remoteManifest, update);
+ drewAny = true;
}
- }
-
- private void DrawInstalledDevPluginList()
- {
- var pluginList = this.pluginListInstalled
- .Where(plugin => plugin.IsDev)
- .ToList();
-
- if (pluginList.Count == 0)
+
+ if (!drewAny)
{
- ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoInstalled);
- return;
- }
+ var text = filter switch
+ {
+ InstalledPluginListFilter.None => Locs.TabBody_NoPluginsInstalled,
+ InstalledPluginListFilter.Testing => Locs.TabBody_NoPluginsTesting,
+ InstalledPluginListFilter.Updateable => Locs.TabBody_NoPluginsUpdateable,
+ InstalledPluginListFilter.Dev => Locs.TabBody_NoPluginsDev,
+ _ => throw new ArgumentException(null, nameof(filter)),
+ };
+
+ ImGuiHelpers.ScaledDummy(60);
- var filteredList = pluginList
- .Where(plugin => !this.IsManifestFiltered(plugin.Manifest))
- .ToList();
-
- if (filteredList.Count == 0)
- {
- ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching);
- return;
- }
-
- var i = 0;
- foreach (var plugin in filteredList)
- {
- this.DrawInstalledPlugin(plugin, i++, null);
+ using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
+ {
+ foreach (var line in text.Split('\n'))
+ {
+ ImGuiHelpers.CenteredText(line);
+ }
+ }
}
}
@@ -1349,29 +1373,29 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
- for (var groupIdx = 0; groupIdx < this.categoryManager.GroupList.Length; groupIdx++)
+ foreach (var groupInfo in this.categoryManager.GroupList)
{
- var groupInfo = this.categoryManager.GroupList[groupIdx];
var canShowGroup = (groupInfo.GroupKind != PluginCategoryManager.GroupKind.DevTools) || this.hasDevPlugins;
if (!canShowGroup)
{
continue;
}
- ImGui.SetNextItemOpen(groupIdx == this.categoryManager.CurrentGroupIdx);
- if (ImGui.CollapsingHeader(groupInfo.Name, groupIdx == this.categoryManager.CurrentGroupIdx ? ImGuiTreeNodeFlags.OpenOnDoubleClick : ImGuiTreeNodeFlags.None))
+ var isCurrent = groupInfo.GroupKind == this.categoryManager.CurrentGroupKind;
+ ImGui.SetNextItemOpen(isCurrent);
+ if (ImGui.CollapsingHeader(groupInfo.Name, isCurrent ? ImGuiTreeNodeFlags.OpenOnDoubleClick : ImGuiTreeNodeFlags.None))
{
- if (this.categoryManager.CurrentGroupIdx != groupIdx)
+ if (!isCurrent)
{
- this.categoryManager.CurrentGroupIdx = groupIdx;
+ this.categoryManager.CurrentGroupKind = groupInfo.GroupKind;
}
ImGui.Indent();
var categoryItemSize = new Vector2(ImGui.GetContentRegionAvail().X - (5 * ImGuiHelpers.GlobalScale), ImGui.GetTextLineHeight());
- for (var categoryIdx = 0; categoryIdx < groupInfo.Categories.Count; categoryIdx++)
+ foreach (var categoryKind in groupInfo.Categories)
{
- var categoryInfo = Array.Find(this.categoryManager.CategoryList, x => x.CategoryId == groupInfo.Categories[categoryIdx]);
-
+ var categoryInfo = this.categoryManager.CategoryList.First(x => x.CategoryKind == categoryKind);
+
switch (categoryInfo.Condition)
{
case PluginCategoryManager.CategoryInfo.AppearCondition.None:
@@ -1385,15 +1409,15 @@ internal class PluginInstallerWindow : Window, IDisposable
throw new ArgumentOutOfRangeException();
}
- var hasSearchHighlight = this.categoryManager.IsCategoryHighlighted(categoryInfo.CategoryId);
+ var hasSearchHighlight = this.categoryManager.IsCategoryHighlighted(categoryInfo.CategoryKind);
if (hasSearchHighlight)
{
ImGui.PushStyleColor(ImGuiCol.Text, colorSearchHighlight);
}
- if (ImGui.Selectable(categoryInfo.Name, this.categoryManager.CurrentCategoryIdx == categoryIdx, ImGuiSelectableFlags.None, categoryItemSize))
+ if (ImGui.Selectable(categoryInfo.Name, this.categoryManager.CurrentCategoryKind == categoryKind, ImGuiSelectableFlags.None, categoryItemSize))
{
- this.categoryManager.CurrentCategoryIdx = categoryIdx;
+ this.categoryManager.CurrentCategoryKind = categoryKind;
}
if (hasSearchHighlight)
@@ -1403,11 +1427,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
ImGui.Unindent();
-
- if (groupIdx != this.categoryManager.GroupList.Length - 1)
- {
- ImGuiHelpers.ScaledDummy(5);
- }
+ ImGuiHelpers.ScaledDummy(5);
}
}
}
@@ -1443,7 +1463,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, ImGuiHelpers.ScaledVector2(1, 3));
- var groupInfo = this.categoryManager.GroupList[this.categoryManager.CurrentGroupIdx];
+ var groupInfo = this.categoryManager.CurrentGroup;
if (this.categoryManager.IsContentDirty)
{
// reset opened list of collapsibles when switching between categories
@@ -1460,53 +1480,65 @@ internal class PluginInstallerWindow : Window, IDisposable
{
case PluginCategoryManager.GroupKind.DevTools:
// this one is never sorted and remains in hardcoded order from group ctor
- switch (this.categoryManager.CurrentCategoryIdx)
+ switch (this.categoryManager.CurrentCategoryKind)
{
- case 0:
- this.DrawInstalledDevPluginList();
+ case PluginCategoryManager.CategoryKind.DevInstalled:
+ this.DrawInstalledPluginList(InstalledPluginListFilter.Dev);
break;
- case 1:
+ case PluginCategoryManager.CategoryKind.IconTester:
this.DrawImageTester();
break;
default:
- // umm, there's nothing else, keep handled set and just skip drawing...
+ ImGui.TextUnformatted("You found a mysterious category. Please keep it to yourself.");
break;
}
break;
case PluginCategoryManager.GroupKind.Installed:
- switch (this.categoryManager.CurrentCategoryIdx)
+ switch (this.categoryManager.CurrentCategoryKind)
{
- case 0:
- this.DrawInstalledPluginList(false);
+ case PluginCategoryManager.CategoryKind.All:
+ this.DrawInstalledPluginList(InstalledPluginListFilter.None);
break;
- case 1:
- this.DrawInstalledPluginList(true);
+ case PluginCategoryManager.CategoryKind.IsTesting:
+ this.DrawInstalledPluginList(InstalledPluginListFilter.Testing);
+ break;
+
+ case PluginCategoryManager.CategoryKind.UpdateablePlugins:
+ this.DrawInstalledPluginList(InstalledPluginListFilter.Updateable);
break;
- case 2:
+ case PluginCategoryManager.CategoryKind.PluginProfiles:
this.profileManagerWidget.Draw();
break;
+
+ default:
+ ImGui.TextUnformatted("You found a secret category. Please feel a sense of pride and accomplishment.");
+ break;
}
break;
case PluginCategoryManager.GroupKind.Changelog:
- switch (this.categoryManager.CurrentCategoryIdx)
+ switch (this.categoryManager.CurrentCategoryKind)
{
- case 0:
+ case PluginCategoryManager.CategoryKind.All:
this.DrawChangelogList(true, true);
break;
- case 1:
+ case PluginCategoryManager.CategoryKind.DalamudChangelogs:
this.DrawChangelogList(true, false);
break;
- case 2:
+ case PluginCategoryManager.CategoryKind.PluginChangelogs:
this.DrawChangelogList(false, true);
break;
+
+ default:
+ ImGui.TextUnformatted("You found a quiet category. Please don't wake it up.");
+ break;
}
break;
@@ -2358,7 +2390,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
- private void DrawInstalledPlugin(LocalPlugin plugin, int index, RemotePluginManifest? remoteManifest, bool showInstalled = false)
+ private void DrawInstalledPlugin(LocalPlugin plugin, int index, RemotePluginManifest? remoteManifest, AvailablePluginUpdate? availablePluginUpdate, bool showInstalled = false)
{
var configuration = Service.Get();
var commandManager = Service.Get();
@@ -2417,8 +2449,6 @@ internal class PluginInstallerWindow : Window, IDisposable
trouble = true;
}
- var availablePluginUpdate = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin);
-
// Dev plugins can never update
if (plugin.IsDev)
availablePluginUpdate = null;
@@ -3576,7 +3606,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{
if (string.IsNullOrEmpty(this.searchText))
{
- this.categoryManager.SetCategoryHighlightsForPlugins(null);
+ this.categoryManager.SetCategoryHighlightsForPlugins(Array.Empty());
// Reset here for good measure, as we're returning from a search
this.openPluginCollapsibles.Clear();
@@ -3717,7 +3747,16 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string TabBody_DownloadFailed => Loc.Localize("InstallerDownloadFailed", "Download failed.");
public static string TabBody_SafeMode => Loc.Localize("InstallerSafeMode", "Dalamud is running in Plugin Safe Mode, restart to activate plugins.");
-
+
+ public static string TabBody_NoPluginsTesting => Loc.Localize("InstallerNoPluginsTesting", "You aren't testing any plugins at the moment!\nYou can opt in to testing versions in the plugin context menu.");
+
+ public static string TabBody_NoPluginsInstalled =>
+ string.Format(Loc.Localize("InstallerNoPluginsInstalled", "You don't have any plugins installed yet!\nYou can install them from the \"{0}\" tab."), PluginCategoryManager.Locs.Category_All);
+
+ public static string TabBody_NoPluginsUpdateable => Loc.Localize("InstallerNoPluginsUpdate", "No plugins have updates available at the moment.");
+
+ public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them some the settings.");
+
#endregion
#region Search text
diff --git a/Dalamud/NativeMethods.txt b/Dalamud/NativeMethods.txt
index c42e76c1c..d9d05e472 100644
--- a/Dalamud/NativeMethods.txt
+++ b/Dalamud/NativeMethods.txt
@@ -11,3 +11,13 @@ SetActiveWindow
HWND_TOPMOST
HWND_NOTOPMOST
SET_WINDOW_POS_FLAGS
+
+OpenClipboard
+SetClipboardData
+CloseClipboard
+DROPFILES
+CLIPBOARD_FORMAT
+GlobalAlloc
+GlobalLock
+GlobalUnlock
+GLOBAL_ALLOC_FLAGS
diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
index 04b1aa48d..cc4626aa4 100644
--- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
+++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
@@ -11,6 +11,7 @@ using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
+using Dalamud.Interface.ImGuiNotification.EventArgs;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.DesignSystem;
@@ -39,7 +40,13 @@ internal class AutoUpdateManager : IServiceType
///
/// Time we should wait between scheduled update checks.
///
- private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(1.5);
+ private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(2);
+
+ ///
+ /// Time we should wait between scheduled update checks if the user has dismissed the notification,
+ /// instead of updating. We don't want to spam the user with notifications.
+ ///
+ private static readonly TimeSpan TimeBetweenUpdateChecksIfDismissed = TimeSpan.FromHours(12);
///
/// Time we should wait after unblocking to nag the user.
@@ -62,12 +69,13 @@ internal class AutoUpdateManager : IServiceType
private readonly IConsoleVariable isDryRun;
private DateTime? loginTime;
- private DateTime? lastUpdateCheckTime;
+ private DateTime? nextUpdateCheckTime;
private DateTime? unblockedSince;
private bool hasStartedInitialUpdateThisSession;
private IActiveNotification? updateNotification;
+ private bool notificationHasStartedUpdate; // Used to track if the user has started an update from the notification.
private Task? autoUpdateTask;
@@ -95,7 +103,7 @@ internal class AutoUpdateManager : IServiceType
});
console.AddCommand("dalamud.autoupdate.force_check", "Force a check for updates", () =>
{
- this.lastUpdateCheckTime = DateTime.Now - TimeBetweenUpdateChecks;
+ this.nextUpdateCheckTime = DateTime.Now + TimeSpan.FromSeconds(5);
return true;
});
}
@@ -127,6 +135,17 @@ internal class AutoUpdateManager : IServiceType
_ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null),
};
}
+
+ private static void DrawOpenInstallerNotificationButton(bool primary, PluginInstallerOpenKind kind, IActiveNotification notification)
+ {
+ if (primary ?
+ DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller) :
+ DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller))
+ {
+ Service.Get().OpenPluginInstallerTo(kind);
+ notification.DismissNow();
+ }
+ }
private void OnUpdate(IFramework framework)
{
@@ -170,11 +189,9 @@ internal class AutoUpdateManager : IServiceType
// 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;
@@ -186,12 +203,13 @@ internal class AutoUpdateManager : IServiceType
if (behavior == AutoUpdateBehavior.OnlyNotify)
{
// List all plugins in the notification
- Log.Verbose("Ran initial update, notifying for {Num} plugins", currentlyUpdatablePlugins.Count);
+ Log.Verbose("Running initial auto-update, notifying for {Num} plugins", currentlyUpdatablePlugins.Count);
this.NotifyUpdatesAreAvailable(currentlyUpdatablePlugins);
return;
}
- Log.Verbose("Ran initial update, updating {Num} plugins", currentlyUpdatablePlugins.Count);
+ Log.Verbose("Running initial auto-update, updating {Num} plugins", currentlyUpdatablePlugins.Count);
+ this.notificationHasStartedUpdate = true;
this.KickOffAutoUpdates(currentlyUpdatablePlugins);
return;
}
@@ -199,10 +217,13 @@ internal class AutoUpdateManager : IServiceType
// 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.nextUpdateCheckTime != null &&
+ DateTime.Now > this.nextUpdateCheckTime &&
this.updateNotification == null)
{
+ this.nextUpdateCheckTime = null;
+
+ Log.Verbose("Starting periodic update check");
this.pluginManager.ReloadPluginMastersAsync()
.ContinueWith(
t =>
@@ -216,8 +237,6 @@ internal class AutoUpdateManager : IServiceType
this.GetAvailablePluginUpdates(
DecideUpdateListingRestriction(behavior)));
});
-
- this.lastUpdateCheckTime = DateTime.Now;
}
}
@@ -226,16 +245,34 @@ internal class AutoUpdateManager : IServiceType
if (this.updateNotification != null)
throw new InvalidOperationException("Already showing a notification");
+ if (this.notificationHasStartedUpdate)
+ throw new InvalidOperationException("Lost track of notification state");
+
this.updateNotification = this.notificationManager.AddNotification(notification);
- this.updateNotification.Dismiss += _ => this.updateNotification = null;
+ this.updateNotification.Dismiss += _ =>
+ {
+ this.updateNotification = null;
+
+ // If the user just clicked off the notification, we don't want to bother them again for quite a while.
+ if (this.notificationHasStartedUpdate)
+ {
+ this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
+ Log.Verbose("User started update, next check at {Time}", this.nextUpdateCheckTime);
+ }
+ else
+ {
+ this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecksIfDismissed;
+ Log.Verbose("User dismissed update notification, next check at {Time}", this.nextUpdateCheckTime);
+ }
+ };
return this.updateNotification!;
}
- private void KickOffAutoUpdates(ICollection updatablePlugins)
+ private void KickOffAutoUpdates(ICollection updatablePlugins, IActiveNotification? notification = null)
{
this.autoUpdateTask =
- Task.Run(() => this.RunAutoUpdates(updatablePlugins))
+ Task.Run(() => this.RunAutoUpdates(updatablePlugins, notification))
.ContinueWith(t =>
{
if (t.IsFaulted)
@@ -252,31 +289,29 @@ internal class AutoUpdateManager : IServiceType
});
}
- private async Task RunAutoUpdates(ICollection updatablePlugins)
+ private async Task RunAutoUpdates(ICollection updatablePlugins, IActiveNotification? notification = null)
{
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,
- });
+ notification ??= this.GetBaseNotification(new Notification());
+ notification.Title = Locs.NotificationTitleUpdatingPlugins;
+ notification.Content = Locs.NotificationContentPreparingToUpdate(updatablePlugins.Count);
+ notification.Type = NotificationType.Info;
+ notification.InitialDuration = TimeSpan.MaxValue;
+ notification.ShowIndeterminateIfNoExpiry = false;
+ notification.UserDismissable = false;
+ notification.Progress = 0;
+ notification.Icon = INotificationIcon.From(FontAwesomeIcon.Download);
+ notification.Minimized = false;
var progress = new Progress();
- progress.ProgressChanged += (_, progress) =>
+ progress.ProgressChanged += (_, updateProgress) =>
{
- notification.Content = Locs.NotificationContentUpdating(progress.CurrentPluginManifest.Name);
- notification.Progress = (float)progress.PluginsProcessed / progress.TotalPlugins;
+ notification.Content = Locs.NotificationContentUpdating(updateProgress.CurrentPluginManifest.Name);
+ notification.Progress = (float)updateProgress.PluginsProcessed / updateProgress.TotalPlugins;
};
var pluginStates = await this.pluginManager.UpdatePluginsAsync(updatablePlugins, this.isDryRun.Value, true, progress);
@@ -288,11 +323,7 @@ internal class AutoUpdateManager : IServiceType
notification.DrawActions += _ =>
{
ImGuiHelpers.ScaledDummy(2);
- if (DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller))
- {
- Service.Get().OpenPluginInstaller();
- notification.DismissNow();
- }
+ DrawOpenInstallerNotificationButton(true, PluginInstallerOpenKind.InstalledPlugins, notification);
};
// Update the notification to show the final state
@@ -328,6 +359,8 @@ internal class AutoUpdateManager : IServiceType
{
if (updatablePlugins.Count == 0)
return;
+
+ this.notificationHasStartedUpdate = false;
var notification = this.GetBaseNotification(new Notification
{
@@ -340,23 +373,22 @@ internal class AutoUpdateManager : IServiceType
Icon = INotificationIcon.From(FontAwesomeIcon.Download),
});
- notification.DrawActions += _ =>
+ void DrawNotificationContent(INotificationDrawArgs args)
{
ImGuiHelpers.ScaledDummy(2);
if (DalamudComponents.PrimaryButton(Locs.NotificationButtonUpdate))
{
- this.KickOffAutoUpdates(updatablePlugins);
- notification.DismissNow();
+ notification.DrawActions -= DrawNotificationContent;
+ this.KickOffAutoUpdates(updatablePlugins, notification);
+ this.notificationHasStartedUpdate = true;
}
ImGui.SameLine();
- if (DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller))
- {
- Service.Get().OpenPluginInstaller();
- notification.DismissNow();
- }
- };
+ DrawOpenInstallerNotificationButton(false, PluginInstallerOpenKind.UpdateablePlugins, notification);
+ }
+
+ notification.DrawActions += DrawNotificationContent;
}
private List GetAvailablePluginUpdates(UpdateListingRestriction restriction)
@@ -440,18 +472,24 @@ internal class AutoUpdateManager : IServiceType
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);
+ => numUpdates == 1 ?
+ Loc.Localize("AutoUpdateUpdatesAvailableContentSingular", "There is a plugin that can be updated.") :
+ string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContentPlural", "There are {0} plugins that can be updated."), numUpdates);
public static string NotificationContentUpdatesAvailableMinimized(int numUpdates)
- => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "{0} updates available."), numUpdates);
+ => numUpdates == 1 ?
+ Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") :
+ string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedPlural", "{0} plugin updates available"), numUpdates);
public static string NotificationContentPreparingToUpdate(int numPlugins)
- => string.Format(Loc.Localize("AutoUpdatePreparingToUpdate", "Preparing to update {0} plugins..."), numPlugins);
+ => numPlugins == 1 ?
+ Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") :
+ string.Format(Loc.Localize("AutoUpdatePreparingToUpdatePlural", "Preparing to update {0} plugins..."), numPlugins);
public static string NotificationContentUpdating(string name)
=> string.Format(Loc.Localize("AutoUpdateUpdating", "Updating {0}..."), name);
public static string NotificationContentFailedPlugins(IEnumerable failedPlugins)
- => string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugins: {0}"), string.Join(", ", failedPlugins));
+ => string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugin(s): {0}"), string.Join(", ", failedPlugins));
}
}
diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs
index 60d2bbe28..64d3d8b93 100644
--- a/Dalamud/Plugin/Internal/PluginManager.cs
+++ b/Dalamud/Plugin/Internal/PluginManager.cs
@@ -1003,9 +1003,6 @@ internal class PluginManager : IInternalDisposableService
if (plugin.InstalledPlugin.IsDev)
continue;
- if (!plugin.InstalledPlugin.IsWantedByAnyProfile)
- continue;
-
if (plugin.InstalledPlugin.Manifest.ScheduledForDeletion)
continue;
diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
index 7d9b79e9b..c6bceed65 100644
--- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
+++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
@@ -483,6 +483,7 @@ internal class LocalPlugin : IDisposable
{
case PluginState.Unloaded:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
+ case PluginState.DependencyResolutionFailed:
case PluginState.UnloadError:
if (!this.IsDev)
{
@@ -501,31 +502,42 @@ internal class LocalPlugin : IDisposable
}
this.State = PluginState.Unloading;
- Log.Information($"Unloading {this.DllFile.Name}");
+ Log.Information("Unloading {PluginName}", this.InternalName);
- if (this.manifest.CanUnloadAsync || framework == null)
- this.instance?.Dispose();
- else
- await framework.RunOnFrameworkThread(() => this.instance?.Dispose());
-
- this.instance = null;
- this.UnloadAndDisposeState();
-
- if (!reloading)
+ try
{
- if (waitBeforeLoaderDispose && this.loader != null)
- await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
- this.loader?.Dispose();
- this.loader = null;
+ if (this.manifest.CanUnloadAsync || framework == null)
+ this.instance?.Dispose();
+ else
+ await framework.RunOnFrameworkThread(() => this.instance?.Dispose());
+ }
+ catch (Exception e)
+ {
+ this.State = PluginState.UnloadError;
+ Log.Error(e, "Could not unload {PluginName}, error in plugin dispose", this.InternalName);
+ return;
+ }
+ finally
+ {
+ this.instance = null;
+ this.UnloadAndDisposeState();
+
+ if (!reloading)
+ {
+ if (waitBeforeLoaderDispose && this.loader != null)
+ await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
+ this.loader?.Dispose();
+ this.loader = null;
+ }
}
this.State = PluginState.Unloaded;
- Log.Information($"Finished unloading {this.DllFile.Name}");
+ Log.Information("Finished unloading {PluginName}", this.InternalName);
}
catch (Exception ex)
{
this.State = PluginState.UnloadError;
- Log.Error(ex, $"Error while unloading {this.Name}");
+ Log.Error(ex, "Error while unloading {PluginName}", this.InternalName);
throw;
}
diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs
index 112427cf0..348efbac1 100644
--- a/Dalamud/Utility/Util.cs
+++ b/Dalamud/Utility/Util.cs
@@ -25,6 +25,11 @@ using Lumina.Excel.GeneratedSheets;
using Serilog;
using TerraFX.Interop.Windows;
using Windows.Win32.Storage.FileSystem;
+using Windows.Win32.System.Memory;
+using Windows.Win32.System.Ole;
+
+using HWND = Windows.Win32.Foundation.HWND;
+using Win32_PInvoke = Windows.Win32.PInvoke;
namespace Dalamud.Utility;
@@ -769,6 +774,69 @@ public static class Util
}
}
+ ///
+ /// Copy files to the clipboard as if they were copied in Explorer.
+ ///
+ /// Full paths to files to be copied.
+ /// Returns true on success.
+ internal static unsafe bool CopyFilesToClipboard(IEnumerable paths)
+ {
+ var pathBytes = paths
+ .Select(Encoding.Unicode.GetBytes)
+ .ToArray();
+ var pathBytesSize = pathBytes
+ .Select(bytes => bytes.Length)
+ .Sum();
+ var sizeWithTerminators = pathBytesSize + (pathBytes.Length * 2);
+
+ var dropFilesSize = sizeof(DROPFILES);
+ var hGlobal = Win32_PInvoke.GlobalAlloc_SafeHandle(
+ GLOBAL_ALLOC_FLAGS.GHND,
+ // struct size + size of encoded strings + null terminator for each
+ // string + two null terminators for end of list
+ (uint)(dropFilesSize + sizeWithTerminators + 4));
+ var dropFiles = (DROPFILES*)Win32_PInvoke.GlobalLock(hGlobal);
+
+ *dropFiles = default;
+ dropFiles->fWide = true;
+ dropFiles->pFiles = (uint)dropFilesSize;
+
+ var pathLoc = (byte*)((nint)dropFiles + dropFilesSize);
+ foreach (var bytes in pathBytes)
+ {
+ // copy the encoded strings
+ for (var i = 0; i < bytes.Length; i++)
+ {
+ pathLoc![i] = bytes[i];
+ }
+
+ // null terminate
+ pathLoc![bytes.Length] = 0;
+ pathLoc[bytes.Length + 1] = 0;
+ pathLoc += bytes.Length + 2;
+ }
+
+ // double null terminator for end of list
+ for (var i = 0; i < 4; i++)
+ {
+ pathLoc![i] = 0;
+ }
+
+ Win32_PInvoke.GlobalUnlock(hGlobal);
+
+ if (Win32_PInvoke.OpenClipboard(HWND.Null))
+ {
+ Win32_PInvoke.SetClipboardData(
+ (uint)CLIPBOARD_FORMAT.CF_HDROP,
+ hGlobal);
+ Win32_PInvoke.CloseClipboard();
+ return true;
+ }
+
+ hGlobal.Dispose();
+ return false;
+ }
+
private static void ShowSpanProperty(ulong addr, IList path, PropertyInfo p, object obj)
{
var objType = obj.GetType();
diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp
index d4e9f0a1c..4b1d4a6e5 100644
--- a/DalamudCrashHandler/DalamudCrashHandler.cpp
+++ b/DalamudCrashHandler/DalamudCrashHandler.cpp
@@ -965,7 +965,7 @@ int main() {
const TASKDIALOG_BUTTON buttons[]{
{IdButtonRestart, L"Restart\nRestart the game with the above-selected option."},
{IdButtonSaveTsPack, L"Save Troubleshooting Info\nSave a .tspack file containing information about this crash for analysis."},
- {IdButtonExit, L"Exit\nExit the game."},
+ {IdButtonExit, L"Exit\nExit without doing anything."},
};
config.cbSize = sizeof(config);
@@ -1060,12 +1060,21 @@ int main() {
pProgressDialog->Release();
pProgressDialog = NULL;
}
+
+ const auto kill_game = [&] { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); };
if (shutup) {
- TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode);
+ kill_game();
return 0;
}
+#if !_DEBUG
+ // In release mode, we can't resume the game, so just kill it. It's not safe to keep it running, as we
+ // don't know what state it's in and it may have crashed off-thread.
+ // Additionally, if the main thread crashed, Windows will show the ANR dialog, which will block our dialog.
+ kill_game();
+#endif
+
int nButtonPressed = 0, nRadioButton = 0;
if (FAILED(TaskDialogIndirect(&config, &nButtonPressed, &nRadioButton, nullptr))) {
SetEvent(exinfo.hEventHandle);
@@ -1073,7 +1082,7 @@ int main() {
switch (nButtonPressed) {
case IdButtonRestart:
{
- TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode);
+ kill_game();
restart_game_using_injector(nRadioButton, *launcherArgs);
break;
}
@@ -1081,7 +1090,7 @@ int main() {
if (attemptResume)
SetEvent(exinfo.hEventHandle);
else
- TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode);
+ kill_game();
}
}
}