From 1abaeef5ab183efb225cda4a62a252936f484555 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 18:54:02 +0200 Subject: [PATCH 01/16] feat: use WorkingPluginId as identifier for plugins to load from profiles --- .../PluginInstaller/PluginInstallerWindow.cs | 24 ++++----- .../PluginInstaller/ProfileManagerWidget.cs | 16 +++--- Dalamud/Plugin/Internal/PluginManager.cs | 47 +++++++++------- Dalamud/Plugin/Internal/Profiles/Profile.cs | 54 +++++++++++++------ .../Internal/Profiles/ProfileManager.cs | 42 ++++++++++----- .../Plugin/Internal/Profiles/ProfileModel.cs | 36 +++++++++++-- .../Internal/Profiles/ProfileModelV1.cs | 2 + .../Internal/Profiles/ProfilePluginEntry.cs | 5 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- 9 files changed, 155 insertions(+), 73 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index dcbdced28..163e62b78 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2381,10 +2381,10 @@ internal class PluginInstallerWindow : Window, IDisposable var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev; var profilesThatWantThisPlugin = profileManager.Profiles - .Where(x => x.WantsPlugin(plugin.InternalName) != null) + .Where(x => x.WantsPlugin(plugin.Manifest.WorkingPluginId) != null) .ToArray(); var isInSingleProfile = profilesThatWantThisPlugin.Length == 1; - var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName); + var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.WorkingPluginId); // Disable everything if the updater is running or another plugin is operating var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress; @@ -2419,17 +2419,17 @@ internal class PluginInstallerWindow : Window, IDisposable foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile)) { - var inProfile = profile.WantsPlugin(plugin.Manifest.InternalName) != null; + var inProfile = profile.WantsPlugin(plugin.Manifest.WorkingPluginId) != null; if (ImGui.Checkbox($"###profilePick{profile.Guid}{plugin.Manifest.InternalName}", ref inProfile)) { if (inProfile) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd); } else { - Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName)) + Task.Run(() => profile.RemoveAsync(plugin.Manifest.WorkingPluginId)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotRemove); } } @@ -2449,11 +2449,11 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, plugin.IsLoaded, false)) + Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.IsLoaded, false)) .GetAwaiter().GetResult(); foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName))) { - Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName, false)) + Task.Run(() => profile.RemoveAsync(plugin.Manifest.WorkingPluginId, false)) .GetAwaiter().GetResult(); } @@ -2527,7 +2527,7 @@ internal class PluginInstallerWindow : Window, IDisposable { await plugin.UnloadAsync(); await applicableProfile.AddOrUpdateAsync( - plugin.Manifest.InternalName, false, false); + plugin.Manifest.WorkingPluginId, false, false); notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); }).ContinueWith(t => @@ -2544,7 +2544,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; - await applicableProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); await plugin.LoadAsync(PluginLoadReason.Installer); notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success); @@ -2565,7 +2565,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (shouldUpdate) { // We need to update the profile right here, because PM will not enable the plugin otherwise - await applicableProfile.AddOrUpdateAsync(plugin.InternalName, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); await this.UpdateSinglePlugin(availableUpdate); } else @@ -2739,7 +2739,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (localPlugin is LocalDevPlugin plugin) { var isInDefaultProfile = - Service.Get().IsInDefaultProfile(localPlugin.Manifest.InternalName); + Service.Get().IsInDefaultProfile(localPlugin.Manifest.WorkingPluginId); // https://colorswall.com/palette/2868/ var greenColor = new Vector4(0x5C, 0xB8, 0x5C, 0xFF) / 0xFF; @@ -3083,7 +3083,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name)); var profman = Service.Get(); - this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.InternalName).CompareTo(profman.IsInDefaultProfile(p2.InternalName))); + this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.Manifest.WorkingPluginId).CompareTo(profman.IsInDefaultProfile(p2.Manifest.WorkingPluginId))); break; default: throw new InvalidEnumArgumentException("Unknown plugin sort type."); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 039877158..2be074f84 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -229,7 +229,7 @@ internal class ProfileManagerWidget if (ImGuiComponents.IconButton($"###exportButton{profile.Guid}", FontAwesomeIcon.FileExport)) { - ImGui.SetClipboardText(profile.Model.Serialize()); + ImGui.SetClipboardText(profile.Model.SerializeForShare()); Service.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success); } @@ -300,7 +300,7 @@ internal class ProfileManagerWidget if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}")) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } } @@ -327,7 +327,7 @@ internal class ProfileManagerWidget if (ImGuiComponents.IconButton(FontAwesomeIcon.FileExport)) { - ImGui.SetClipboardText(profile.Model.Serialize()); + ImGui.SetClipboardText(profile.Model.SerializeForShare()); Service.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success); } @@ -400,7 +400,7 @@ internal class ProfileManagerWidget if (pluginListChild) { var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale; - string? wantRemovePluginInternalName = null; + Guid? wantRemovePluginGuid = null; using var syncScope = profile.GetSyncScope(); foreach (var plugin in profile.Plugins.ToArray()) @@ -467,7 +467,7 @@ internal class ProfileManagerWidget var enabled = plugin.IsEnabled; if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.InternalName, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.WorkingPluginId, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } @@ -477,17 +477,17 @@ internal class ProfileManagerWidget if (ImGuiComponents.IconButton($"###removePlugin{plugin.InternalName}", FontAwesomeIcon.Trash)) { - wantRemovePluginInternalName = plugin.InternalName; + wantRemovePluginGuid = plugin.WorkingPluginId; } if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.RemovePlugin); } - if (wantRemovePluginInternalName != null) + if (wantRemovePluginGuid != null) { // TODO: handle error - Task.Run(() => profile.RemoveAsync(wantRemovePluginInternalName, false)) + Task.Run(() => profile.RemoveAsync(wantRemovePluginGuid.Value, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove); } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 691d5f729..f782b4129 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1290,13 +1290,27 @@ internal partial class PluginManager : IDisposable, IServiceType if (isDev) { Log.Information($"Loading dev plugin {name}"); - var devPlugin = new LocalDevPlugin(dllFile, manifest); + plugin = new LocalDevPlugin(dllFile, manifest); + } + else + { + Log.Information($"Loading plugin {name}"); + plugin = new LocalPlugin(dllFile, manifest); + } + + // Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here. + if (plugin.Manifest.WorkingPluginId == Guid.Empty) + throw new Exception("Plugin should have a WorkingPluginId at this point"); + this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId); + + // Now, if this is a devPlugin, figure out if we want to load it + if (isDev) + { + var devPlugin = (LocalDevPlugin)plugin; loadPlugin &= !isBoot; - var probablyInternalNameForThisPurpose = manifest?.InternalName ?? dllFile.Name; - var wantsInDefaultProfile = - this.profileManager.DefaultProfile.WantsPlugin(probablyInternalNameForThisPurpose); + this.profileManager.DefaultProfile.WantsPlugin(plugin.Manifest.WorkingPluginId); if (wantsInDefaultProfile == null) { // We don't know about this plugin, so we don't want to do anything here. @@ -1305,46 +1319,41 @@ internal partial class PluginManager : IDisposable, IServiceType else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) { // We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled. - Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot) { // We wanted this plugin, and StartOnBoot is on. That means we actually do want it. - Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, true, false); + Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); loadPlugin = !doNotLoad; } else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot) { // We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore. - Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot) { // We didn't want this plugin, and StartOnBoot is off. We don't want it. - Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); loadPlugin = false; } plugin = devPlugin; } - else - { - Log.Information($"Loading plugin {name}"); - plugin = new LocalPlugin(dllFile, manifest); - } #pragma warning disable CS0618 var defaultState = manifest?.Disabled != true && loadPlugin; #pragma warning restore CS0618 - + // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.InternalName, defaultState); + var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, defaultState); if (loadPlugin) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index ac46d9153..657cde534 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -102,7 +102,7 @@ internal class Profile /// Gets all plugins declared in this profile. /// public IEnumerable Plugins => - this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.IsEnabled)); + this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.WorkingPluginId, x.IsEnabled)); /// /// Gets this profile's underlying model. @@ -144,11 +144,11 @@ internal class Profile /// /// The internal name of the plugin. /// Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled. - public bool? WantsPlugin(string internalName) + public bool? WantsPlugin(Guid workingPluginId) { lock (this) { - var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + var entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); return entry?.IsEnabled; } } @@ -161,13 +161,13 @@ internal class Profile /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. - public async Task AddOrUpdateAsync(string internalName, bool state, bool apply = true) + public async Task AddOrUpdateAsync(Guid workingPluginId, bool state, bool apply = true) { - Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()"); - + Debug.Assert(workingPluginId != Guid.Empty, "Trying to add plugin with empty guid"); + lock (this) { - var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + var existing = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); if (existing != null) { existing.IsEnabled = state; @@ -176,16 +176,16 @@ internal class Profile { this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin { - InternalName = internalName, + WorkingPluginId = workingPluginId, IsEnabled = state, }); } } // We need to remove this plugin from the default profile, if it declares it. - if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(internalName) != null) + if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(workingPluginId) != null) { - await this.manager.DefaultProfile.RemoveAsync(internalName, false); + await this.manager.DefaultProfile.RemoveAsync(workingPluginId, false); } Service.Get().QueueSave(); @@ -201,25 +201,25 @@ internal class Profile /// The internal name of the plugin. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. - public async Task RemoveAsync(string internalName, bool apply = true) + public async Task RemoveAsync(Guid workingPluginId, bool apply = true) { ProfileModelV1.ProfileModelV1Plugin entry; lock (this) { - entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); if (entry == null) - throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\""); + throw new ArgumentException($"No plugin \"{workingPluginId}\" in profile \"{this.Guid}\""); if (!this.modelV1.Plugins.Remove(entry)) throw new Exception("Couldn't remove plugin from model collection"); } // We need to add this plugin back to the default profile, if we were the last profile to have it. - if (!this.manager.IsInAnyProfile(internalName)) + if (!this.manager.IsInAnyProfile(workingPluginId)) { if (!this.IsDefaultProfile) { - await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, this.IsEnabled && entry.IsEnabled, false); + await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, this.IsEnabled && entry.IsEnabled, false); } else { @@ -233,6 +233,30 @@ internal class Profile await this.manager.ApplyAllWantStatesAsync(); } + /// + /// This function tries to migrate all plugins with this internalName which do not have + /// a GUID to the specified GUID. + /// This is best-effort and will probably work well for anyone that only uses regular plugins. + /// + /// InternalName of the plugin to migrate. + /// Guid to use. + public void MigrateProfilesToGuidsForPlugin(string internalName, Guid newGuid) + { + lock (this) + { + foreach (var plugin in this.modelV1.Plugins) + { + if (plugin.InternalName == internalName && plugin.WorkingPluginId == Guid.Empty) + { + plugin.WorkingPluginId = newGuid; + Log.Information("Migrated profile {Profile} plugin {Name} to guid {Guid}", this, internalName, newGuid); + } + } + } + + Service.Get().QueueSave(); + } + /// public override string ToString() => $"{this.Guid} ({this.Name})"; } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 46b572c1a..1d14ade4b 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -73,7 +73,7 @@ internal class ProfileManager : IServiceType /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. - public async Task GetWantStateAsync(string internalName, bool defaultState, bool addIfNotDeclared = true) + public async Task GetWantStateAsync(Guid workingPluginId, bool defaultState, bool addIfNotDeclared = true) { var want = false; var wasInAnyProfile = false; @@ -82,7 +82,7 @@ internal class ProfileManager : IServiceType { foreach (var profile in this.profiles) { - var state = profile.WantsPlugin(internalName); + var state = profile.WantsPlugin(workingPluginId); if (state.HasValue) { want = want || (profile.IsEnabled && state.Value); @@ -93,8 +93,8 @@ internal class ProfileManager : IServiceType if (!wasInAnyProfile && addIfNotDeclared) { - Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState); - await this.DefaultProfile.AddOrUpdateAsync(internalName, defaultState, false); + Log.Warning("{Guid} was not in any profile, adding to default with {Default}", workingPluginId, defaultState); + await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, defaultState, false); return defaultState; } @@ -107,10 +107,10 @@ internal class ProfileManager : IServiceType /// /// The internal name of the plugin. /// Whether or not the plugin is in any profile. - public bool IsInAnyProfile(string internalName) + public bool IsInAnyProfile(Guid workingPluginId) { lock (this.profiles) - return this.profiles.Any(x => x.WantsPlugin(internalName) != null); + return this.profiles.Any(x => x.WantsPlugin(workingPluginId) != null); } /// @@ -119,8 +119,8 @@ internal class ProfileManager : IServiceType /// /// The internal name of the plugin. /// Whether or not the plugin is in the default profile. - public bool IsInDefaultProfile(string internalName) - => this.DefaultProfile.WantsPlugin(internalName) != null; + public bool IsInDefaultProfile(Guid workingPluginId) + => this.DefaultProfile.WantsPlugin(workingPluginId) != null; /// /// Add a new profile. @@ -151,7 +151,7 @@ internal class ProfileManager : IServiceType /// The newly cloned profile. public Profile CloneProfile(Profile toClone) { - var newProfile = this.ImportProfile(toClone.Model.Serialize()); + var newProfile = this.ImportProfile(toClone.Model.SerializeForShare()); if (newProfile == null) throw new Exception("New profile was null while cloning"); @@ -196,13 +196,13 @@ internal class ProfileManager : IServiceType this.isBusy = true; Log.Information("Getting want states..."); - List wantActive; + List wantActive; lock (this.profiles) { wantActive = this.profiles .Where(x => x.IsEnabled) .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled) - .Select(plugin => plugin.InternalName)) + .Select(plugin => plugin.WorkingPluginId)) .Distinct().ToList(); } @@ -218,7 +218,7 @@ internal class ProfileManager : IServiceType var pm = Service.Get(); foreach (var installedPlugin in pm.InstalledPlugins) { - var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName); + var wantThis = wantActive.Contains(installedPlugin.Manifest.WorkingPluginId); switch (wantThis) { case true when !installedPlugin.IsLoaded: @@ -267,7 +267,7 @@ internal class ProfileManager : IServiceType // We need to remove all plugins from the profile first, so that they are re-added to the default profile if needed foreach (var plugin in profile.Plugins.ToArray()) { - await profile.RemoveAsync(plugin.InternalName, false); + await profile.RemoveAsync(plugin.WorkingPluginId, false); } if (!this.config.SavedProfiles!.Remove(profile.Model)) @@ -279,6 +279,22 @@ internal class ProfileManager : IServiceType this.config.QueueSave(); } + /// + /// This function tries to migrate all plugins with this internalName which do not have + /// a GUID to the specified GUID. + /// This is best-effort and will probably work well for anyone that only uses regular plugins. + /// + /// InternalName of the plugin to migrate. + /// Guid to use. + public void MigrateProfilesToGuidsForPlugin(string internalName, Guid newGuid) + { + lock (this.profiles) + { + foreach (var profile in this.profiles) + profile.MigrateProfilesToGuidsForPlugin(internalName, newGuid); + } + } + private string GenerateUniqueProfileName(string startingWith) { if (this.profiles.All(x => x.Name != startingWith)) diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs index bf2a9c2c9..d77cab443 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs @@ -1,7 +1,9 @@ using System; - +using System.Collections.Generic; +using System.Reflection; using Dalamud.Utility; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace Dalamud.Plugin.Internal.Profiles; @@ -39,11 +41,11 @@ public abstract class ProfileModel } /// - /// Serialize this model into a string usable for sharing. + /// Serialize this model into a string usable for sharing, without including GUIDs. /// /// The serialized representation of the model. /// Thrown when an unsupported model is serialized. - public string Serialize() + public string SerializeForShare() { string prefix; switch (this) @@ -55,6 +57,32 @@ public abstract class ProfileModel throw new ArgumentOutOfRangeException(); } - return prefix + Convert.ToBase64String(Util.CompressString(JsonConvert.SerializeObject(this))); + // HACK: Just filter the ID for now, we should split the sharing + saving model + var serialized = JsonConvert.SerializeObject(this, new JsonSerializerSettings() + { ContractResolver = new IgnorePropertiesResolver(new[] { "WorkingPluginId" }) }); + + return prefix + Convert.ToBase64String(Util.CompressString(serialized)); + } + + // Short helper class to ignore some properties from serialization + private class IgnorePropertiesResolver : DefaultContractResolver + { + private readonly HashSet ignoreProps; + + public IgnorePropertiesResolver(IEnumerable propNamesToIgnore) + { + this.ignoreProps = new HashSet(propNamesToIgnore); + } + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + if (this.ignoreProps.Contains(property.PropertyName)) + { + property.ShouldSerialize = _ => false; + } + + return property; + } } } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index 2a851d234..1b224c8dc 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -46,6 +46,8 @@ public class ProfileModelV1 : ProfileModel /// Gets or sets the internal name of the plugin. /// public string? InternalName { get; set; } + + public Guid WorkingPluginId { get; set; } /// /// Gets or sets a value indicating whether or not this entry is enabled. diff --git a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs index 0a6f5140b..2c10def99 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs @@ -10,9 +10,10 @@ internal class ProfilePluginEntry /// /// The internal name of the plugin. /// A value indicating whether or not this entry is enabled. - public ProfilePluginEntry(string internalName, bool state) + public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state) { this.InternalName = internalName; + this.WorkingPluginId = workingPluginId; this.IsEnabled = state; } @@ -20,6 +21,8 @@ internal class ProfilePluginEntry /// Gets the internal name of the plugin. /// public string InternalName { get; } + + public Guid WorkingPluginId { get; set; } /// /// Gets a value indicating whether or not this entry is enabled. diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index f7306b5a7..8abfd2f9f 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -235,7 +235,7 @@ internal class LocalPlugin : IDisposable /// INCLUDES the default profile. /// public bool IsWantedByAnyProfile => - Service.Get().GetWantStateAsync(this.manifest.InternalName, false, false).GetAwaiter().GetResult(); + Service.Get().GetWantStateAsync(this.manifest.WorkingPluginId, false, false).GetAwaiter().GetResult(); /// /// Gets a value indicating whether this plugin's API level is out of date. From a9f6d6d104080148068c2b422968135ea439ce47 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:04:08 +0200 Subject: [PATCH 02/16] chore: ModuleLog fmt objects may be nullable --- Dalamud/Logging/Internal/ModuleLog.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index 2fb735640..baa2708ac 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -33,7 +33,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Verbose(string messageTemplate, params object[] values) + public void Verbose(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, null, values); /// @@ -42,7 +42,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Verbose(Exception exception, string messageTemplate, params object[] values) + public void Verbose(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values); /// @@ -50,7 +50,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Debug(string messageTemplate, params object[] values) + public void Debug(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, null, values); /// @@ -59,7 +59,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Debug(Exception exception, string messageTemplate, params object[] values) + public void Debug(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values); /// @@ -67,7 +67,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Information(string messageTemplate, params object[] values) + public void Information(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, null, values); /// @@ -76,7 +76,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Information(Exception exception, string messageTemplate, params object[] values) + public void Information(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values); /// @@ -84,7 +84,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Warning(string messageTemplate, params object[] values) + public void Warning(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, null, values); /// @@ -93,7 +93,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Warning(Exception exception, string messageTemplate, params object[] values) + public void Warning(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values); /// @@ -101,7 +101,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Error(string messageTemplate, params object[] values) + public void Error(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, null, values); /// @@ -110,7 +110,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Error(Exception? exception, string messageTemplate, params object[] values) + public void Error(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values); /// @@ -118,7 +118,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Fatal(string messageTemplate, params object[] values) + public void Fatal(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, null, values); /// @@ -127,11 +127,11 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Fatal(Exception exception, string messageTemplate, params object[] values) + public void Fatal(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); private void WriteLog( - LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) + LogEventLevel level, string messageTemplate, Exception? exception = null, params object?[] values) { // FIXME: Eventually, the `pluginName` tag should be removed from here and moved over to the actual log // formatter. From 6e54c085fa32a14a11699f6cea86b114f81c8394 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:08:58 +0200 Subject: [PATCH 03/16] fix: find matching plugins when importing a profile --- Dalamud/Plugin/Internal/PluginManager.cs | 3 +++ Dalamud/Plugin/Internal/Profiles/Profile.cs | 4 ++++ .../Internal/Profiles/ProfileManager.cs | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index f782b4129..37dab0f03 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1299,6 +1299,9 @@ internal partial class PluginManager : IDisposable, IServiceType } // Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here. + // This will also happen if you are installing a plugin with the installer, and that's intended! + // It means that, if you have a profile which has unsatisfied plugins, installing a matching plugin will + // enter it into the profiles it can match. if (plugin.Manifest.WorkingPluginId == Guid.Empty) throw new Exception("Plugin should have a WorkingPluginId at this point"); this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId); diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 657cde534..b9c90235a 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -246,6 +246,10 @@ internal class Profile { foreach (var plugin in this.modelV1.Plugins) { + // TODO: What should happen if a profile has a GUID locked in, but the plugin + // is not installed anymore? That probably means that the user uninstalled the plugin + // and is now reinstalling it. We should still satisfy that and update the ID. + if (plugin.InternalName == internalName && plugin.WorkingPluginId == Guid.Empty) { plugin.WorkingPluginId = newGuid; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 1d14ade4b..d8f091e9f 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -172,7 +172,27 @@ internal class ProfileManager : IServiceType newModel.Guid = Guid.NewGuid(); newModel.Name = this.GenerateUniqueProfileName(newModel.Name.IsNullOrEmpty() ? "Unknown Collection" : newModel.Name); if (newModel is ProfileModelV1 modelV1) + { + // Disable it modelV1.IsEnabled = false; + + // Try to find matching plugins for all plugins in the profile + var pm = Service.Get(); + foreach (var plugin in modelV1.Plugins) + { + var installedPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName); + if (installedPlugin != null) + { + Log.Information("Satisfying plugin {InternalName} for profile {Name} with {Guid}", plugin.InternalName, newModel.Name, installedPlugin.Manifest.WorkingPluginId); + plugin.WorkingPluginId = installedPlugin.Manifest.WorkingPluginId; + } + else + { + Log.Warning("Couldn't find plugin {InternalName} for profile {Name}", plugin.InternalName, newModel.Name); + plugin.WorkingPluginId = Guid.Empty; + } + } + } this.config.SavedProfiles!.Add(newModel); this.config.QueueSave(); From 8b85139e6198ad22257aa1938d866c8e79a262a7 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:16:54 +0200 Subject: [PATCH 04/16] chore: prevent plugins from being installed twice for now if an assignment is missing --- .../PluginInstaller/ProfileManagerWidget.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 2be074f84..7c9026505 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -406,7 +406,7 @@ internal class ProfileManagerWidget foreach (var plugin in profile.Plugins.ToArray()) { didAny = true; - var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName); + var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == plugin.WorkingPluginId); var btnOffset = 2; if (pmPlugin != null) @@ -437,26 +437,33 @@ internal class ProfileManagerWidget ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); ImGui.TextUnformatted(text); - - var available = + + var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == plugin.InternalName); + var installable = pm.AvailablePlugins.FirstOrDefault( x => x.InternalName == plugin.InternalName && !x.SourceRepo.IsThirdParty); - if (available != null) + + if (firstAvailableInstalled != null) + { + // TODO + ImGui.Text("GOAT WAS TOO LAZY TO IMPLEMENT THIS"); + } + else if (installable != null) { ImGui.SameLine(); ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 2) - 2); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); btnOffset = 3; - if (ImGuiComponents.IconButton($"###installMissingPlugin{available.InternalName}", FontAwesomeIcon.Download)) + if (ImGuiComponents.IconButton($"###installMissingPlugin{installable.InternalName}", FontAwesomeIcon.Download)) { - this.installer.StartInstall(available, false); + this.installer.StartInstall(installable, false); } if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.InstallPlugin); } - + ImGui.SetCursorPos(before); } From a85c6315d4da311b5f9368eeb58358ac530e8f24 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:24:30 +0200 Subject: [PATCH 05/16] warnings --- Dalamud/Plugin/Internal/Profiles/Profile.cs | 6 +++--- Dalamud/Plugin/Internal/Profiles/ProfileManager.cs | 6 +++--- Dalamud/Plugin/Internal/Profiles/ProfileModel.cs | 4 ++-- Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs | 3 +++ Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs | 4 ++++ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index b9c90235a..d20b5c6bc 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -142,7 +142,7 @@ internal class Profile /// /// Check if this profile contains a specific plugin, and if it is enabled. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled. public bool? WantsPlugin(Guid workingPluginId) { @@ -157,7 +157,7 @@ internal class Profile /// Add a plugin to this profile with the desired state, or change the state of a plugin in this profile. /// This will block until all states have been applied. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. @@ -198,7 +198,7 @@ internal class Profile /// Remove a plugin from this profile. /// This will block until all states have been applied. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. public async Task RemoveAsync(Guid workingPluginId, bool apply = true) diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index d8f091e9f..6b51f7535 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -69,7 +69,7 @@ internal class ProfileManager : IServiceType /// /// Check if any enabled profile wants a specific plugin enabled. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. @@ -105,7 +105,7 @@ internal class ProfileManager : IServiceType /// /// Check whether a plugin is declared in any profile. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the plugin is in any profile. public bool IsInAnyProfile(Guid workingPluginId) { @@ -117,7 +117,7 @@ internal class ProfileManager : IServiceType /// Check whether a plugin is only in the default profile. /// A plugin can never be in the default profile if it is in any other profile. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the plugin is in the default profile. public bool IsInDefaultProfile(Guid workingPluginId) => this.DefaultProfile.WantsPlugin(workingPluginId) != null; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs index d77cab443..e3d9e2955 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Reflection; + using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index 1b224c8dc..99da4263b 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -47,6 +47,9 @@ public class ProfileModelV1 : ProfileModel /// public string? InternalName { get; set; } + /// + /// Gets or sets an ID uniquely identifying this specific instance of a plugin. + /// public Guid WorkingPluginId { get; set; } /// diff --git a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs index 2c10def99..7909981bc 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs @@ -9,6 +9,7 @@ internal class ProfilePluginEntry /// Initializes a new instance of the class. /// /// The internal name of the plugin. + /// The ID of the plugin. /// A value indicating whether or not this entry is enabled. public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state) { @@ -22,6 +23,9 @@ internal class ProfilePluginEntry /// public string InternalName { get; } + /// + /// Gets or sets an ID uniquely identifying this specific instance of a plugin. + /// public Guid WorkingPluginId { get; set; } /// From b3740d0539e5a03d4a2a5c64c479be7c3ee10dd5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:03:14 +0100 Subject: [PATCH 06/16] add Profile.RemoveByInternalNameAsync() --- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- Dalamud/Plugin/Internal/Profiles/Profile.cs | 36 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 20e2ea7af..5d250a533 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1297,7 +1297,7 @@ internal partial class PluginManager : IDisposable, IServiceType try { // We don't need to apply, it doesn't matter - await this.profileManager.DefaultProfile.RemoveAsync(repoManifest.InternalName, false); + await this.profileManager.DefaultProfile.RemoveByInternalNameAsync(repoManifest.InternalName, false); } catch (ProfileOperationException) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 3e7a2ed55..36cafa29b 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -208,7 +208,7 @@ internal class Profile { entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); if (entry == null) - throw new PluginNotFoundException(workingPluginId.ToString()); + throw new PluginNotFoundException(workingPluginId); if (!this.modelV1.Plugins.Remove(entry)) throw new Exception("Couldn't remove plugin from model collection"); @@ -233,6 +233,31 @@ internal class Profile await this.manager.ApplyAllWantStatesAsync(); } + /// + /// Remove a plugin from this profile. + /// This will block until all states have been applied. + /// + /// The internal name of the plugin. + /// Whether or not the current state should immediately be applied. + /// A representing the asynchronous operation. + public async Task RemoveByInternalNameAsync(string internalName, bool apply = true) + { + Guid? pluginToRemove = null; + lock (this) + { + foreach (var plugin in this.Plugins) + { + if (plugin.InternalName.Equals(internalName, StringComparison.Ordinal)) + { + pluginToRemove = plugin.WorkingPluginId; + break; + } + } + } + + await this.RemoveAsync(pluginToRemove ?? throw new PluginNotFoundException(internalName), apply); + } + /// /// This function tries to migrate all plugins with this internalName which do not have /// a GUID to the specified GUID. @@ -308,4 +333,13 @@ internal sealed class PluginNotFoundException : ProfileOperationException : base($"The plugin '{internalName}' was not found in the profile") { } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the plugin causing the error. + public PluginNotFoundException(Guid workingPluginId) + : base($"The plugin '{workingPluginId}' was not found in the profile") + { + } } From d827151ee550cb0500690dd1ee56ca7c6f501f98 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:21:37 +0100 Subject: [PATCH 07/16] add icon for dev plugins --- Dalamud/DalamudAsset.cs | 19 ++++++---- .../Internal/Windows/PluginImageCache.cs | 6 ++++ .../PluginInstaller/PluginInstallerWindow.cs | 8 +++++ .../PluginInstaller/ProfileManagerWidget.cs | 35 ++++++++++++------- Dalamud/Logging/Internal/ModuleLog.cs | 10 +++--- 5 files changed, 55 insertions(+), 23 deletions(-) diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index 184193796..a7b35b196 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -63,41 +63,48 @@ public enum DalamudAsset [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "troubleIcon.png")] TroubleIcon = 1006, + + /// + /// : The plugin trouble icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "devPluginIcon.png")] + DevPluginIcon = 1007, /// /// : The plugin update icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "updateIcon.png")] - UpdateIcon = 1007, + UpdateIcon = 1008, /// /// : The plugin installed icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "installedIcon.png")] - InstalledIcon = 1008, + InstalledIcon = 1009, /// /// : The third party plugin icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "thirdIcon.png")] - ThirdIcon = 1009, + ThirdIcon = 1010, /// /// : The installed third party plugin icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "thirdInstalledIcon.png")] - ThirdInstalledIcon = 1010, + ThirdInstalledIcon = 1011, /// /// : The API bump explainer icon. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "changelogApiBump.png")] - ChangelogApiBumpIcon = 1011, + ChangelogApiBumpIcon = 1012, /// /// : The background shade for @@ -105,7 +112,7 @@ public enum DalamudAsset /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "tsmShade.png")] - TitleScreenMenuShade = 1012, + TitleScreenMenuShade = 1013, /// /// : Noto Sans CJK JP Medium. diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 528507229..29adbb3e5 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -98,6 +98,12 @@ internal class PluginImageCache : IDisposable, IServiceType /// public IDalamudTextureWrap TroubleIcon => this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TroubleIcon, this.EmptyTexture); + + /// + /// Gets the devPlugin icon overlay. + /// + public IDalamudTextureWrap DevPluginIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon, this.EmptyTexture); /// /// Gets the plugin update icon overlay. diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 6db48405d..240383695 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1800,6 +1800,14 @@ internal class PluginInstallerWindow : Window, IDisposable var isLoaded = plugin is { IsLoaded: true }; + if (plugin is LocalDevPlugin) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); + ImGui.Image(this.imageCache.DevPluginIcon.ImGuiHandle, iconSize); + ImGui.PopStyleVar(); + ImGui.SetCursorPos(cursorBeforeImage); + } + if (updateAvailable) ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); else if ((trouble && !pluginDisabled) || isOrphan) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index def5f8ce8..26006c84a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Profiles; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; using Serilog; @@ -315,13 +316,13 @@ internal class ProfileManagerWidget if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80))) { // TODO: Plugin searching should be abstracted... installer and this should use the same search - foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && !x.IsDev && + foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && (this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant())))) { using var disabled2 = ImRaii.Disabled(profile.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName)); - if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}")) + if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}")) { Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); @@ -426,18 +427,28 @@ internal class ProfileManagerWidget Guid? wantRemovePluginGuid = null; using var syncScope = profile.GetSyncScope(); - foreach (var plugin in profile.Plugins.ToArray()) + foreach (var profileEntry in profile.Plugins.ToArray()) { didAny = true; - var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == plugin.WorkingPluginId); + var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == profileEntry.WorkingPluginId); var btnOffset = 2; if (pmPlugin != null) { + var cursorBeforeIcon = ImGui.GetCursorPos(); pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon); icon ??= pic.DefaultIcon; ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight)); + + if (pmPlugin is LocalDevPlugin) + { + ImGui.SetCursorPos(cursorBeforeIcon); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); + ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); + ImGui.PopStyleVar(); + } + ImGui.SameLine(); var text = $"{pmPlugin.Name}"; @@ -454,17 +465,17 @@ internal class ProfileManagerWidget ImGui.Image(pic.DefaultIcon.ImGuiHandle, new Vector2(pluginLineHeight)); ImGui.SameLine(); - var text = Locs.NotInstalled(plugin.InternalName); + var text = Locs.NotInstalled(profileEntry.InternalName); var textHeight = ImGui.CalcTextSize(text); var before = ImGui.GetCursorPos(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); ImGui.TextUnformatted(text); - var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == plugin.InternalName); + var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == profileEntry.InternalName); var installable = pm.AvailablePlugins.FirstOrDefault( - x => x.InternalName == plugin.InternalName && !x.SourceRepo.IsThirdParty); + x => x.InternalName == profileEntry.InternalName && !x.SourceRepo.IsThirdParty); if (firstAvailableInstalled != null) { @@ -494,10 +505,10 @@ internal class ProfileManagerWidget ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30)); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); - var enabled = plugin.IsEnabled; - if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled)) + var enabled = profileEntry.IsEnabled; + if (ImGui.Checkbox($"###{this.editingProfileGuid}-{profileEntry.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.WorkingPluginId, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } @@ -505,9 +516,9 @@ internal class ProfileManagerWidget ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * btnOffset) - 5); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); - if (ImGuiComponents.IconButton($"###removePlugin{plugin.InternalName}", FontAwesomeIcon.Trash)) + if (ImGuiComponents.IconButton($"###removePlugin{profileEntry.InternalName}", FontAwesomeIcon.Trash)) { - wantRemovePluginGuid = plugin.WorkingPluginId; + wantRemovePluginGuid = profileEntry.WorkingPluginId; } if (ImGui.IsItemHovered()) diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index e59db09d3..1fe955294 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -43,7 +43,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Verbose(Exception exception, string messageTemplate, params object?[] values) + public void Verbose(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values); /// @@ -62,7 +62,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Debug(Exception exception, string messageTemplate, params object?[] values) + public void Debug(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values); /// @@ -81,7 +81,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Information(Exception exception, string messageTemplate, params object?[] values) + public void Information(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values); /// @@ -100,7 +100,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Warning(Exception exception, string messageTemplate, params object?[] values) + public void Warning(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values); /// @@ -138,7 +138,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Fatal(Exception exception, string messageTemplate, params object?[] values) + public void Fatal(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); [MessageTemplateFormatMethod("messageTemplate")] From 256f4989f7795a15f47badcb3f4b8e2bb0e628db Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:39:18 +0100 Subject: [PATCH 08/16] add some validation code to catch issues --- Dalamud/Plugin/Internal/PluginManager.cs | 33 +++++++++++++++++++ .../Internal/Profiles/ProfileManager.cs | 31 +++++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 5d250a533..7af530ee9 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -664,6 +664,15 @@ internal partial class PluginManager : IDisposable, IServiceType this.PluginsReady = true; this.NotifyinstalledPluginsListChanged(); sigScanner.Save(); + + try + { + this.ParanoiaValidatePluginsAndProfiles(); + } + catch (Exception ex) + { + Log.Error(ex, "Plugin and profile validation failed!"); + } }, tokenSource.Token); } @@ -1256,6 +1265,30 @@ internal partial class PluginManager : IDisposable, IServiceType } } + /// + /// Check if there are any inconsistencies with our plugins, their IDs, and our profiles. + /// + private void ParanoiaValidatePluginsAndProfiles() + { + var seenIds = new List(); + + foreach (var installedPlugin in this.InstalledPlugins) + { + if (installedPlugin.Manifest.WorkingPluginId == Guid.Empty) + throw new Exception($"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has an empty WorkingPluginId."); + + if (seenIds.Contains(installedPlugin.Manifest.WorkingPluginId)) + { + throw new Exception( + $"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has a duplicate WorkingPluginId '{installedPlugin.Manifest.WorkingPluginId}'"); + } + + seenIds.Add(installedPlugin.Manifest.WorkingPluginId); + } + + this.profileManager.ParanoiaValidateProfiles(); + } + private async Task DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting) { var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 6b51f7535..768583bea 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -216,19 +216,18 @@ internal class ProfileManager : IServiceType this.isBusy = true; Log.Information("Getting want states..."); - List wantActive; + List wantActive; lock (this.profiles) { wantActive = this.profiles .Where(x => x.IsEnabled) - .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled) - .Select(plugin => plugin.WorkingPluginId)) + .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled)) .Distinct().ToList(); } - foreach (var internalName in wantActive) + foreach (var profilePluginEntry in wantActive) { - Log.Information("\t=> Want {Name}", internalName); + Log.Information("\t=> Want {Name}({WorkingPluginId})", profilePluginEntry.InternalName, profilePluginEntry.WorkingPluginId); } Log.Information("Applying want states..."); @@ -238,7 +237,7 @@ internal class ProfileManager : IServiceType var pm = Service.Get(); foreach (var installedPlugin in pm.InstalledPlugins) { - var wantThis = wantActive.Contains(installedPlugin.Manifest.WorkingPluginId); + var wantThis = wantActive.Any(x => x.WorkingPluginId == installedPlugin.Manifest.WorkingPluginId); switch (wantThis) { case true when !installedPlugin.IsLoaded: @@ -314,6 +313,26 @@ internal class ProfileManager : IServiceType profile.MigrateProfilesToGuidsForPlugin(internalName, newGuid); } } + + /// + /// Validate profiles for errors. + /// + /// Thrown when a profile is not sane. + public void ParanoiaValidateProfiles() + { + foreach (var profile in this.profiles) + { + var seenIds = new List(); + + foreach (var pluginEntry in profile.Plugins) + { + if (seenIds.Contains(pluginEntry.WorkingPluginId)) + throw new Exception($"Plugin '{pluginEntry.WorkingPluginId}'('{pluginEntry.InternalName}') is twice in profile '{profile.Guid}'('{profile.Name}')"); + + seenIds.Add(pluginEntry.WorkingPluginId); + } + } + } private string GenerateUniqueProfileName(string startingWith) { From 9024c9b00c8ec826c47468f01a8485647af3fb84 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:47:56 +0100 Subject: [PATCH 09/16] track internal name nonetheless --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 10 +++++----- .../Windows/PluginInstaller/ProfileManagerWidget.cs | 4 ++-- Dalamud/Plugin/Internal/PluginManager.cs | 10 +++++----- Dalamud/Plugin/Internal/Profiles/Profile.cs | 6 ++++-- Dalamud/Plugin/Internal/Profiles/ProfileManager.cs | 7 ++++--- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 240383695..1545efb65 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2579,7 +2579,7 @@ internal class PluginInstallerWindow : Window, IDisposable { if (inProfile) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd); } else @@ -2604,7 +2604,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.IsLoaded, false)) + Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, plugin.IsLoaded, false)) .GetAwaiter().GetResult(); foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName))) { @@ -2682,7 +2682,7 @@ internal class PluginInstallerWindow : Window, IDisposable { await plugin.UnloadAsync(); await applicableProfile.AddOrUpdateAsync( - plugin.Manifest.WorkingPluginId, false, false); + plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); }).ContinueWith(t => @@ -2699,7 +2699,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; - await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false); await plugin.LoadAsync(PluginLoadReason.Installer); notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success); @@ -2720,7 +2720,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (shouldUpdate) { // We need to update the profile right here, because PM will not enable the plugin otherwise - await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false); await this.UpdateSinglePlugin(availableUpdate); } else diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 26006c84a..62806404a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -324,7 +324,7 @@ internal class ProfileManagerWidget if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}")) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } } @@ -508,7 +508,7 @@ internal class ProfileManagerWidget var enabled = profileEntry.IsEnabled; if (ImGui.Checkbox($"###{this.editingProfileGuid}-{profileEntry.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, profileEntry.InternalName, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 7af530ee9..c57487d1d 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1511,28 +1511,28 @@ internal partial class PluginManager : IDisposable, IServiceType { // We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled. Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot) { // We wanted this plugin, and StartOnBoot is on. That means we actually do want it. Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false); loadPlugin = !doNotLoad; } else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot) { // We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore. Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot) { // We didn't want this plugin, and StartOnBoot is off. We don't want it. Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); loadPlugin = false; } @@ -1544,7 +1544,7 @@ internal partial class PluginManager : IDisposable, IServiceType #pragma warning restore CS0618 // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, defaultState); + var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); if (loadPlugin) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 36cafa29b..df5b045e2 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -158,10 +158,11 @@ internal class Profile /// This will block until all states have been applied. /// /// The ID of the plugin. + /// The internal name of the plugin, if available. /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. - public async Task AddOrUpdateAsync(Guid workingPluginId, bool state, bool apply = true) + public async Task AddOrUpdateAsync(Guid workingPluginId, string? internalName, bool state, bool apply = true) { Debug.Assert(workingPluginId != Guid.Empty, "Trying to add plugin with empty guid"); @@ -176,6 +177,7 @@ internal class Profile { this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin { + InternalName = internalName, WorkingPluginId = workingPluginId, IsEnabled = state, }); @@ -219,7 +221,7 @@ internal class Profile { if (!this.IsDefaultProfile) { - await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, this.IsEnabled && entry.IsEnabled, false); + await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, entry.InternalName, this.IsEnabled && entry.IsEnabled, false); } else { diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 768583bea..10d94de73 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -70,10 +70,11 @@ internal class ProfileManager : IServiceType /// Check if any enabled profile wants a specific plugin enabled. /// /// The ID of the plugin. + /// The internal name of the plugin, if available. /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. - public async Task GetWantStateAsync(Guid workingPluginId, bool defaultState, bool addIfNotDeclared = true) + public async Task GetWantStateAsync(Guid workingPluginId, string? internalName, bool defaultState, bool addIfNotDeclared = true) { var want = false; var wasInAnyProfile = false; @@ -93,8 +94,8 @@ internal class ProfileManager : IServiceType if (!wasInAnyProfile && addIfNotDeclared) { - Log.Warning("{Guid} was not in any profile, adding to default with {Default}", workingPluginId, defaultState); - await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, defaultState, false); + Log.Warning("'{Guid}'('{InternalName}') was not in any profile, adding to default with {Default}", workingPluginId, internalName, defaultState); + await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, internalName, defaultState, false); return defaultState; } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 348563781..0f65bafb2 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -164,7 +164,7 @@ internal class LocalPlugin : IDisposable /// INCLUDES the default profile. /// public bool IsWantedByAnyProfile => - Service.Get().GetWantStateAsync(this.manifest.WorkingPluginId, false, false).GetAwaiter().GetResult(); + Service.Get().GetWantStateAsync(this.manifest.WorkingPluginId, this.Manifest.InternalName, false, false).GetAwaiter().GetResult(); /// /// Gets a value indicating whether this plugin's API level is out of date. From 23ddc7824126efccc226e36d6a8cf328a992757a Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:53:17 +0100 Subject: [PATCH 10/16] add bodge "match to plugin" UI for installed plugins --- .../PluginInstaller/ProfileManagerWidget.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 62806404a..2d45869e0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -479,8 +479,22 @@ internal class ProfileManagerWidget if (firstAvailableInstalled != null) { - // TODO - ImGui.Text("GOAT WAS TOO LAZY TO IMPLEMENT THIS"); + ImGui.Text($"Match to plugin '{firstAvailableInstalled.Name}'?"); + ImGui.SameLine(); + if (ImGuiComponents.IconButtonWithText( + FontAwesomeIcon.Check, + "Yes, use this one")) + { + profileEntry.WorkingPluginId = firstAvailableInstalled.Manifest.WorkingPluginId; + Task.Run(async () => + { + await profman.ApplyAllWantStatesAsync(); + }) + .ContinueWith(t => + { + this.installer.DisplayErrorContinuation(t, Locs.ErrorCouldNotChangeState); + }); + } } else if (installable != null) { From b415f5a8741f6590ccc3ad64e643b17edeb283ae Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:12:32 +0100 Subject: [PATCH 11/16] never offer updates for dev plugins --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 1545efb65..b2fa50a03 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2246,6 +2246,11 @@ internal class PluginInstallerWindow : Window, IDisposable } var availablePluginUpdate = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin); + + // Dev plugins can never update + if (plugin.IsDev) + availablePluginUpdate = null; + // Update available if (availablePluginUpdate != default) { From d26db7e05342b61e95419827537036c835b886d2 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:26:59 +0100 Subject: [PATCH 12/16] don't tell people to wait for an update, if one is available --- .../PluginInstaller/PluginInstallerWindow.cs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index b2fa50a03..0c5437724 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1881,16 +1881,32 @@ internal class PluginInstallerWindow : Window, IDisposable if (plugin is { IsOutdated: true, IsBanned: false } || installableOutdated) { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.TextWrapped(Locs.PluginBody_Outdated); + + var bodyText = Locs.PluginBody_Outdated + " "; + if (updateAvailable) + bodyText += Locs.PluginBody_Outdated_CanNowUpdate; + else + bodyText += Locs.PluginBody_Outdated_WaitForUpdate; + + ImGui.TextWrapped(bodyText); ImGui.PopStyleColor(); } else if (plugin is { IsBanned: true }) { // Banned warning ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGuiHelpers.SafeTextWrapped(plugin.BanReason.IsNullOrEmpty() - ? Locs.PluginBody_Banned - : Locs.PluginBody_BannedReason(plugin.BanReason)); + + var bodyText = plugin.BanReason.IsNullOrEmpty() + ? Locs.PluginBody_Banned + : Locs.PluginBody_BannedReason(plugin.BanReason); + bodyText += " "; + + if (updateAvailable) + bodyText += Locs.PluginBody_Outdated_CanNowUpdate; + else + bodyText += Locs.PluginBody_Outdated_WaitForUpdate; + + ImGuiHelpers.SafeTextWrapped(bodyText); ImGui.PopStyleColor(); } @@ -3497,7 +3513,11 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginBody_Plugin3rdPartyRepo(string url) => Loc.Localize("InstallerPlugin3rdPartyRepo", "From custom plugin repository {0}").Format(url); - public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible at the moment. Please wait for it to be updated by its author."); + public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible."); + + public static string PluginBody_Outdated_WaitForUpdate => Loc.Localize("InstallerOutdatedWaitForUpdate", "Please wait for it to be updated by its author."); + + public static string PluginBody_Outdated_CanNowUpdate => Loc.Localize("InstallerOutdatedCanNowUpdate", "An update is available for installation."); public static string PluginBody_Orphaned => Loc.Localize("InstallerOrphanedPluginBody ", "This plugin's source repository is no longer available. You may need to reinstall it from its repository, or re-add the repository."); @@ -3507,7 +3527,7 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginBody_LoadFailed => Loc.Localize("InstallerLoadFailedPluginBody ", "This plugin failed to load. Please contact the author for more information."); - public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available at the moment. Please wait for it to be updated by its author."); + public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available."); public static string PluginBody_Policy => Loc.Localize("InstallerPolicyPluginBody ", "Plugin loads for this type of plugin were manually disabled."); From 4e95d4fe37e811b031538b2469cdfb060c82e69d Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:32:39 +0100 Subject: [PATCH 13/16] allow load of devPlugins in non-default profile --- Dalamud/Plugin/Internal/PluginManager.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index c57487d1d..b0a421b0d 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1506,6 +1506,16 @@ internal partial class PluginManager : IDisposable, IServiceType { // We don't know about this plugin, so we don't want to do anything here. // The code below will take care of it and add it with the default value. + Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName); + + // If it is wanted by any other plugin, we do want to load it. This means we are looking it up twice, but I don't care right now. + // I am putting a TODO so that goat will clean it up some day soon. + if (await this.profileManager.GetWantStateAsync( + plugin.Manifest.WorkingPluginId, + plugin.Manifest.InternalName, + false, + false)) + loadPlugin = true; } else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) { @@ -1544,19 +1554,20 @@ internal partial class PluginManager : IDisposable, IServiceType #pragma warning restore CS0618 // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); - + var wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); + Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin); + if (loadPlugin) { try { - if (wantToLoad && !plugin.IsOrphaned) + if (wantedByAnyProfile && !plugin.IsOrphaned) { await plugin.LoadAsync(reason); } else { - Log.Verbose($"{name} not loaded, wantToLoad:{wantToLoad} orphaned:{plugin.IsOrphaned}"); + Log.Verbose($"{name} not loaded, wantToLoad:{wantedByAnyProfile} orphaned:{plugin.IsOrphaned}"); } } catch (InvalidPluginException) From 57b8a5d932b0449acd4fe840c6d6c601cbfcf56d Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:42:44 +0100 Subject: [PATCH 14/16] prevent double-lookup for dev plugins in non-default profiles --- Dalamud/Plugin/Internal/PluginManager.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index b0a421b0d..8bfb38c34 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1493,6 +1493,8 @@ internal partial class PluginManager : IDisposable, IServiceType if (plugin.Manifest.WorkingPluginId == Guid.Empty) throw new Exception("Plugin should have a WorkingPluginId at this point"); this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId); + + var wantedByAnyProfile = false; // Now, if this is a devPlugin, figure out if we want to load it if (isDev) @@ -1508,13 +1510,12 @@ internal partial class PluginManager : IDisposable, IServiceType // The code below will take care of it and add it with the default value. Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName); - // If it is wanted by any other plugin, we do want to load it. This means we are looking it up twice, but I don't care right now. - // I am putting a TODO so that goat will clean it up some day soon. - if (await this.profileManager.GetWantStateAsync( - plugin.Manifest.WorkingPluginId, - plugin.Manifest.InternalName, - false, - false)) + // Check if any profile wants this plugin. We need to do this here, since we want to allow loading a dev plugin if a non-default profile wants it active. + // Note that this will not add the plugin to the default profile. That's done below in any other case. + wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); + + // If it is wanted by any other profile, we do want to load it. + if (wantedByAnyProfile) loadPlugin = true; } else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) @@ -1553,8 +1554,9 @@ internal partial class PluginManager : IDisposable, IServiceType var defaultState = manifest?.Disabled != true && loadPlugin; #pragma warning restore CS0618 - // Need to do this here, so plugins that don't load are still added to the default profile - var wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); + // Plugins that aren't in any profile will be added to the default profile with this call. + // We are skipping a double-lookup for dev plugins that are wanted by non-default profiles, as noted above. + wantedByAnyProfile = wantedByAnyProfile || await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin); if (loadPlugin) From af2f0f290f0a80926740d17cde0eaf190e8ca2c0 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:43:24 +0100 Subject: [PATCH 15/16] dev plugins are now allowed to be in profiles --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0c5437724..0d1a07769 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2555,7 +2555,7 @@ internal class PluginInstallerWindow : Window, IDisposable var profileManager = Service.Get(); var config = Service.Get(); - var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev; + var applicableForProfiles = plugin.Manifest.SupportsProfiles /*&& !plugin.IsDev*/; var profilesThatWantThisPlugin = profileManager.Profiles .Where(x => x.WantsPlugin(plugin.Manifest.WorkingPluginId) != null) .ToArray(); From 4f4f604ef87bece0c4cb113ac74150fbf09e01b4 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 20 Jan 2024 01:10:07 +0100 Subject: [PATCH 16/16] show all plugins - be it dev, installed, available, orphaned - in the available tab --- .../PluginInstaller/PluginInstallerWindow.cs | 89 +++++++++++++------ .../PluginInstaller/ProfileManagerWidget.cs | 6 +- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0d1a07769..5007691ab 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -107,6 +107,7 @@ internal class PluginInstallerWindow : Window, IDisposable private int updatePluginCount = 0; private List? updatedPlugins; + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Makes sense like this")] private List pluginListAvailable = new(); private List pluginListInstalled = new(); private List pluginListUpdatable = new(); @@ -1126,45 +1127,79 @@ internal class PluginInstallerWindow : Window, IDisposable this.DrawChangelog(logEntry); } } - + + private record PluginInstallerAvailablePluginProxy(RemotePluginManifest? RemoteManifest, LocalPlugin? LocalPlugin); + +#pragma warning disable SA1201 private void DrawAvailablePluginList() +#pragma warning restore SA1201 { - var pluginList = this.pluginListAvailable; + var availableManifests = this.pluginListAvailable; + var installedPlugins = this.pluginListInstalled.ToList(); // Copy intended - if (pluginList.Count == 0) + if (availableManifests.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoCompatible); return; } - var filteredManifests = pluginList + var filteredAvailableManifests = availableManifests .Where(rm => !this.IsManifestFiltered(rm)) .ToList(); - if (filteredManifests.Count == 0) + if (filteredAvailableManifests.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching); return; } - // get list to show and reset category dirty flag - var categoryManifestsList = this.categoryManager.GetCurrentCategoryContent(filteredManifests); + var proxies = new List(); + + // Go through all AVAILABLE manifests, associate them with a NON-DEV local plugin, if one is available, and remove it from the pile + foreach (var availableManifest in this.categoryManager.GetCurrentCategoryContent(filteredAvailableManifests).Cast()) + { + var plugin = this.pluginListInstalled.FirstOrDefault(plugin => plugin.Manifest.InternalName == availableManifest.InternalName && plugin.Manifest.RepoUrl == availableManifest.RepoUrl); + + // We "consumed" this plugin from the pile and remove it. + if (plugin != null && !plugin.IsDev) + { + installedPlugins.Remove(plugin); + proxies.Add(new PluginInstallerAvailablePluginProxy(null, plugin)); + + continue; + } + + proxies.Add(new PluginInstallerAvailablePluginProxy(availableManifest, null)); + } + + // Now, add all applicable local plugins that haven't been "used up", in most cases either dev or orphaned plugins. + foreach (var installedPlugin in installedPlugins) + { + if (this.IsManifestFiltered(installedPlugin.Manifest)) + continue; + + // TODO: We should also check categories here, for good measure + + proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin)); + } var i = 0; - foreach (var manifest in categoryManifestsList) + foreach (var proxy in proxies) { - if (manifest is not RemotePluginManifest remoteManifest) - continue; - var (isInstalled, plugin) = this.IsManifestInstalled(remoteManifest); + IPluginManifest applicableManifest = proxy.LocalPlugin != null ? proxy.LocalPlugin.Manifest : proxy.RemoteManifest; - ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}"); - if (isInstalled) + if (applicableManifest == null) + throw new Exception("Could not determine manifest for available plugin"); + + ImGui.PushID($"{applicableManifest.InternalName}{applicableManifest.AssemblyVersion}"); + + if (proxy.LocalPlugin != null) { - this.DrawInstalledPlugin(plugin, i++, true); + this.DrawInstalledPlugin(proxy.LocalPlugin, i++, true); } - else + else if (proxy.RemoteManifest != null) { - this.DrawAvailablePlugin(remoteManifest, i++); + this.DrawAvailablePlugin(proxy.RemoteManifest, i++); } ImGui.PopID(); @@ -1800,14 +1835,6 @@ internal class PluginInstallerWindow : Window, IDisposable var isLoaded = plugin is { IsLoaded: true }; - if (plugin is LocalDevPlugin) - { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); - ImGui.Image(this.imageCache.DevPluginIcon.ImGuiHandle, iconSize); - ImGui.PopStyleVar(); - ImGui.SetCursorPos(cursorBeforeImage); - } - if (updateAvailable) ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); else if ((trouble && !pluginDisabled) || isOrphan) @@ -1836,8 +1863,7 @@ internal class PluginInstallerWindow : Window, IDisposable // Name ImGui.TextUnformatted(label); - // Verified Checkmark, don't show for dev plugins - if (plugin is null or { IsDev: false }) + // Verified Checkmark or dev plugin wrench { ImGui.SameLine(); ImGui.Text(" "); @@ -1847,8 +1873,15 @@ internal class PluginInstallerWindow : Window, IDisposable var unverifiedOutlineColor = KnownColor.Black.Vector(); var verifiedIconColor = KnownColor.RoyalBlue.Vector() with { W = 0.75f }; var unverifiedIconColor = KnownColor.Orange.Vector(); - - if (!isThirdParty) + var devIconOutlineColor = KnownColor.White.Vector(); + var devIconColor = KnownColor.MediumOrchid.Vector(); + + if (plugin is LocalDevPlugin) + { + this.DrawFontawesomeIconOutlined(FontAwesomeIcon.Wrench, devIconOutlineColor, devIconColor); + this.VerifiedCheckmarkFadeTooltip(label, "This is a dev plugin. You added it."); + } + else if (!isThirdParty) { this.DrawFontawesomeIconOutlined(FontAwesomeIcon.CheckCircle, verifiedOutlineColor, verifiedIconColor); this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_VerifiedTooltip); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 2d45869e0..eafea9d16 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -441,17 +441,17 @@ internal class ProfileManagerWidget ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight)); - if (pmPlugin is LocalDevPlugin) + if (pmPlugin.IsDev) { ImGui.SetCursorPos(cursorBeforeIcon); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.7f); ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); ImGui.PopStyleVar(); } ImGui.SameLine(); - var text = $"{pmPlugin.Name}"; + var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}"; var textHeight = ImGui.CalcTextSize(text); var before = ImGui.GetCursorPos();