mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-14 20:54:16 +01:00
282 lines
9.6 KiB
C#
282 lines
9.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
|
|
using CheapLoc;
|
|
using Dalamud.Configuration.Internal;
|
|
using Dalamud.Logging.Internal;
|
|
using Dalamud.Utility;
|
|
|
|
namespace Dalamud.Plugin.Internal.Profiles;
|
|
|
|
/// <summary>
|
|
/// Class responsible for managing plugin profiles.
|
|
/// </summary>
|
|
[ServiceManager.BlockingEarlyLoadedService]
|
|
internal class ProfileManager : IServiceType
|
|
{
|
|
private static readonly ModuleLog Log = new("PROFMAN");
|
|
private readonly DalamudConfiguration config;
|
|
|
|
private readonly List<Profile> profiles = new();
|
|
|
|
private volatile bool isBusy = false;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ProfileManager"/> class.
|
|
/// </summary>
|
|
/// <param name="config">Dalamud config.</param>
|
|
[ServiceManager.ServiceConstructor]
|
|
public ProfileManager(DalamudConfiguration config)
|
|
{
|
|
this.config = config;
|
|
|
|
this.LoadProfilesFromConfigInitially();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the default profile.
|
|
/// </summary>
|
|
public Profile DefaultProfile => this.profiles.First(x => x.IsDefaultProfile);
|
|
|
|
/// <summary>
|
|
/// Gets all profiles, including the default profile.
|
|
/// </summary>
|
|
public IEnumerable<Profile> Profiles => this.profiles;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether or not the profile manager is busy enabling/disabling plugins.
|
|
/// </summary>
|
|
public bool IsBusy => this.isBusy;
|
|
|
|
/// <summary>
|
|
/// Check if any enabled profile wants a specific plugin enabled.
|
|
/// </summary>
|
|
/// <param name="internalName">The internal name of the plugin.</param>
|
|
/// <param name="defaultState">The state the plugin shall be in, if it needs to be added.</param>
|
|
/// <param name="addIfNotDeclared">Whether or not the plugin should be added to the default preset, if it's not present in any preset.</param>
|
|
/// <returns>Whether or not the plugin shall be enabled.</returns>
|
|
public bool GetWantState(string internalName, bool defaultState, bool addIfNotDeclared = true)
|
|
{
|
|
var want = false;
|
|
var wasInAnyProfile = false;
|
|
|
|
foreach (var profile in this.profiles)
|
|
{
|
|
var state = profile.WantsPlugin(internalName);
|
|
if (state.HasValue)
|
|
{
|
|
want = want || (profile.IsEnabled && state.Value);
|
|
wasInAnyProfile = true;
|
|
}
|
|
}
|
|
|
|
if (!wasInAnyProfile && addIfNotDeclared)
|
|
{
|
|
Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState);
|
|
this.DefaultProfile.AddOrUpdate(internalName, defaultState, false);
|
|
return defaultState;
|
|
}
|
|
|
|
return want;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check whether a plugin is declared in any profile.
|
|
/// </summary>
|
|
/// <param name="internalName">The internal name of the plugin.</param>
|
|
/// <returns>Whether or not the plugin is in any profile.</returns>
|
|
public bool IsInAnyProfile(string internalName)
|
|
=> this.profiles.Any(x => x.WantsPlugin(internalName) != null);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="internalName">The internal name of the plugin.</param>
|
|
/// <returns>Whether or not the plugin is in the default profile.</returns>
|
|
public bool IsInDefaultProfile(string internalName)
|
|
=> this.DefaultProfile.WantsPlugin(internalName) != null;
|
|
|
|
/// <summary>
|
|
/// Add a new profile.
|
|
/// </summary>
|
|
/// <returns>The added profile.</returns>
|
|
public Profile AddNewProfile()
|
|
{
|
|
var model = new ProfileModelV1
|
|
{
|
|
Guid = Guid.NewGuid(),
|
|
Name = this.GenerateUniqueProfileName(Loc.Localize("PluginProfilesNewProfile", "New Collection")),
|
|
IsEnabled = false,
|
|
};
|
|
|
|
this.config.SavedProfiles!.Add(model);
|
|
this.config.QueueSave();
|
|
|
|
var profile = new Profile(this, model, false, false);
|
|
this.profiles.Add(profile);
|
|
|
|
return profile;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clone a specified profile.
|
|
/// </summary>
|
|
/// <param name="toClone">The profile to clone.</param>
|
|
/// <returns>The newly cloned profile.</returns>
|
|
public Profile CloneProfile(Profile toClone)
|
|
{
|
|
var newProfile = this.ImportProfile(toClone.Model.Serialize());
|
|
if (newProfile == null)
|
|
throw new Exception("New profile was null while cloning");
|
|
|
|
return newProfile;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Import a profile with a sharing string.
|
|
/// </summary>
|
|
/// <param name="data">The sharing string to import.</param>
|
|
/// <returns>The imported profile, or null, if the string was invalid.</returns>
|
|
public Profile? ImportProfile(string data)
|
|
{
|
|
var newModel = ProfileModel.Deserialize(data);
|
|
if (newModel == null)
|
|
return null;
|
|
|
|
newModel.Guid = Guid.NewGuid();
|
|
newModel.Name = this.GenerateUniqueProfileName(newModel.Name.IsNullOrEmpty() ? "Unknown Collection" : newModel.Name);
|
|
if (newModel is ProfileModelV1 modelV1)
|
|
modelV1.IsEnabled = false;
|
|
|
|
this.config.SavedProfiles!.Add(newModel);
|
|
this.config.QueueSave();
|
|
|
|
var profile = new Profile(this, newModel, false, false);
|
|
this.profiles.Add(profile);
|
|
|
|
return profile;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go through all profiles and plugins, and enable/disable plugins they want active.
|
|
/// This will block until all plugins have been loaded/reloaded.
|
|
/// </summary>
|
|
public void ApplyAllWantStates()
|
|
{
|
|
this.isBusy = true;
|
|
Log.Information("Getting want states...");
|
|
|
|
var wantActive = this.profiles
|
|
.Where(x => x.IsEnabled)
|
|
.SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled)
|
|
.Select(plugin => plugin.InternalName))
|
|
.Distinct().ToList();
|
|
|
|
foreach (var internalName in wantActive)
|
|
{
|
|
Log.Information("\t=> Want {Name}", internalName);
|
|
}
|
|
|
|
Log.Information("Applying want states...");
|
|
|
|
var pm = Service<PluginManager>.Get();
|
|
var tasks = new List<Task>();
|
|
|
|
foreach (var installedPlugin in pm.InstalledPlugins)
|
|
{
|
|
var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName);
|
|
switch (wantThis)
|
|
{
|
|
case true when !installedPlugin.IsLoaded:
|
|
if (installedPlugin.ApplicableForLoad)
|
|
{
|
|
Log.Information("\t=> Enabling {Name}", installedPlugin.Manifest.InternalName);
|
|
tasks.Add(installedPlugin.LoadAsync(PluginLoadReason.Installer));
|
|
}
|
|
else
|
|
{
|
|
Log.Warning("\t=> {Name} wanted active, but not applicable", installedPlugin.Manifest.InternalName);
|
|
}
|
|
|
|
break;
|
|
case false when installedPlugin.IsLoaded:
|
|
Log.Information("\t=> Disabling {Name}", installedPlugin.Manifest.InternalName);
|
|
tasks.Add(installedPlugin.UnloadAsync());
|
|
break;
|
|
}
|
|
}
|
|
|
|
// This is probably not ideal... Might need to rethink the error handling strategy for this.
|
|
try
|
|
{
|
|
Task.WaitAll(tasks.ToArray());
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e, "Couldn't apply state for one or more plugins");
|
|
}
|
|
|
|
Log.Information("Applied!");
|
|
this.isBusy = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete a profile.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// You should definitely apply states after this. It doesn't do it for you.
|
|
/// </remarks>
|
|
/// <param name="profile">The profile to delete.</param>
|
|
public void DeleteProfile(Profile profile)
|
|
{
|
|
// 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())
|
|
{
|
|
profile.Remove(plugin.InternalName, false);
|
|
}
|
|
|
|
if (!this.config.SavedProfiles!.Remove(profile.Model))
|
|
throw new Exception("Couldn't remove profile from models");
|
|
|
|
if (!this.profiles.Remove(profile))
|
|
throw new Exception("Couldn't remove runtime profile");
|
|
|
|
this.config.QueueSave();
|
|
}
|
|
|
|
private string GenerateUniqueProfileName(string startingWith)
|
|
{
|
|
if (this.profiles.All(x => x.Name != startingWith))
|
|
return startingWith;
|
|
|
|
startingWith = Regex.Replace(startingWith, @" \(.* Mix\)", string.Empty);
|
|
|
|
while (true)
|
|
{
|
|
var newName = $"{startingWith} ({CultureInfo.InvariantCulture.TextInfo.ToTitleCase(Util.GetRandomName())} Mix)";
|
|
|
|
if (this.profiles.All(x => x.Name != newName))
|
|
return newName;
|
|
}
|
|
}
|
|
|
|
private void LoadProfilesFromConfigInitially()
|
|
{
|
|
this.config.DefaultProfile ??= new ProfileModelV1();
|
|
this.profiles.Add(new Profile(this, this.config.DefaultProfile, true, true));
|
|
|
|
this.config.SavedProfiles ??= new List<ProfileModel>();
|
|
foreach (var profileModel in this.config.SavedProfiles)
|
|
{
|
|
this.profiles.Add(new Profile(this, profileModel, false, true));
|
|
}
|
|
|
|
this.config.QueueSave();
|
|
}
|
|
}
|