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(); } } }