Code commit
This commit is contained in:
204
CustomizePlus/Profiles/Data/Profile.cs
Normal file
204
CustomizePlus/Profiles/Data/Profile.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Core.Extensions;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
||||
namespace CustomizePlus.Profiles.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates the user-controlled aspects of a character profile, ie all of
|
||||
/// the information that gets saved to disk by the plugin.
|
||||
/// </summary>
|
||||
public sealed class Profile : ISavable
|
||||
{
|
||||
private static int _nextGlobalId;
|
||||
|
||||
private readonly int _localId;
|
||||
|
||||
public List<Armature> Armatures = new();
|
||||
|
||||
public LowerString CharacterName { get; set; } = LowerString.Empty;
|
||||
public LowerString Name { get; set; } = LowerString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to search only through local player owned characters or all characters when searching for game object by name
|
||||
/// </summary>
|
||||
public bool LimitLookupToOwnedObjects { get; set; } = false;
|
||||
|
||||
public int Version { get; set; } = Constants.ConfigurationVersion;
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
public DateTimeOffset CreationDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTimeOffset ModifiedDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public Guid UniqueId { get; set; } = Guid.NewGuid();
|
||||
|
||||
public List<Template> Templates { get; init; } = new();
|
||||
|
||||
public bool IsWriteProtected { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies if this profile is not persistent (ex. was made via IPC calls) and should not be displayed in UI.
|
||||
/// WARNING, TEMPLATES FOR TEMPORARY PROFILES *ARE NOT* STORED IN TemplateManager
|
||||
/// </summary>
|
||||
public bool IsTemporary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identificator specifying specific actor this profile applies to, only works for temporary profiles
|
||||
/// </summary>
|
||||
public ActorIdentifier TemporaryActor { get; set; } = ActorIdentifier.Invalid;
|
||||
|
||||
public string Incognito
|
||||
=> UniqueId.ToString()[..8];
|
||||
|
||||
public Profile()
|
||||
{
|
||||
_localId = _nextGlobalId++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new profile based on data from another one
|
||||
/// </summary>
|
||||
/// <param name="original"></param>
|
||||
public Profile(Profile original) : this()
|
||||
{
|
||||
CharacterName = original.CharacterName;
|
||||
LimitLookupToOwnedObjects = original.LimitLookupToOwnedObjects;
|
||||
|
||||
foreach (var template in original.Templates)
|
||||
{
|
||||
Templates.Add(template);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Profile '{Name.Text.Incognify()}' on {CharacterName.Text.Incognify()} [{UniqueId}]";
|
||||
}
|
||||
|
||||
#region Serialization
|
||||
|
||||
public new JObject JsonSerialize()
|
||||
{
|
||||
var ret = new JObject()
|
||||
{
|
||||
["Version"] = Version,
|
||||
["UniqueId"] = UniqueId,
|
||||
["CreationDate"] = CreationDate,
|
||||
["ModifiedDate"] = ModifiedDate,
|
||||
["CharacterName"] = CharacterName.Text,
|
||||
["Name"] = Name.Text,
|
||||
["LimitLookupToOwnedObjects"] = LimitLookupToOwnedObjects,
|
||||
["Enabled"] = Enabled,
|
||||
["IsWriteProtected"] = IsWriteProtected,
|
||||
["Templates"] = SerializeTemplates()
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private JArray SerializeTemplates()
|
||||
{
|
||||
var ret = new JArray();
|
||||
foreach (var template in Templates)
|
||||
{
|
||||
ret.Add(new JObject()
|
||||
{
|
||||
["TemplateId"] = template.UniqueId
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deserialization
|
||||
|
||||
public static Profile Load(TemplateManager templateManager, JObject obj)
|
||||
{
|
||||
var version = obj["Version"]?.ToObject<int>() ?? 0;
|
||||
return version switch
|
||||
{
|
||||
//Ignore everything below v4 for now
|
||||
4 => LoadV4(templateManager, obj),
|
||||
_ => throw new Exception("The design to be loaded has no valid Version."),
|
||||
};
|
||||
}
|
||||
|
||||
private static Profile LoadV4(TemplateManager templateManager, JObject obj)
|
||||
{
|
||||
var creationDate = obj["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
|
||||
|
||||
var profile = new Profile()
|
||||
{
|
||||
CreationDate = creationDate,
|
||||
UniqueId = obj["UniqueId"]?.ToObject<Guid>() ?? throw new ArgumentNullException("UniqueId"),
|
||||
Name = new LowerString(obj["Name"]?.ToObject<string>() ?? throw new ArgumentNullException("Name")),
|
||||
CharacterName = new LowerString(obj["CharacterName"]?.ToObject<string>() ?? throw new ArgumentNullException("CharacterName")),
|
||||
LimitLookupToOwnedObjects = obj["LimitLookupToOwnedObjects"]?.ToObject<bool>() ?? throw new ArgumentNullException("LimitLookupToOwnedObjects"),
|
||||
Enabled = obj["Enabled"]?.ToObject<bool>() ?? throw new ArgumentNullException("Enabled"),
|
||||
ModifiedDate = obj["ModifiedDate"]?.ToObject<DateTimeOffset>() ?? creationDate,
|
||||
IsWriteProtected = obj["IsWriteProtected"]?.ToObject<bool>() ?? false,
|
||||
Templates = new List<Template>()
|
||||
};
|
||||
if (profile.ModifiedDate < creationDate)
|
||||
profile.ModifiedDate = creationDate;
|
||||
|
||||
if (obj["Templates"] is not JArray templateArray)
|
||||
return profile;
|
||||
|
||||
foreach (var templateObj in templateArray)
|
||||
{
|
||||
if (templateObj is not JObject templateObjCast)
|
||||
{
|
||||
//todo: warning
|
||||
continue;
|
||||
}
|
||||
|
||||
var templateId = templateObjCast["TemplateId"]?.ToObject<Guid>();
|
||||
if (templateId == null)
|
||||
continue; //todo: error
|
||||
|
||||
var template = templateManager.GetTemplate((Guid)templateId);
|
||||
if (template != null)
|
||||
profile.Templates.Add(template);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISavable
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.ProfileFile(this);
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
//saving of temporary profiles is not allowed
|
||||
if (IsTemporary)
|
||||
return;
|
||||
|
||||
using var j = new JsonTextWriter(writer)
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
var obj = JsonSerialize();
|
||||
obj.WriteTo(j);
|
||||
}
|
||||
|
||||
public string LogName(string fileName)
|
||||
=> Path.GetFileNameWithoutExtension(fileName);
|
||||
|
||||
#endregion
|
||||
}
|
||||
46
CustomizePlus/Profiles/Events/ProfileChanged.cs
Normal file
46
CustomizePlus/Profiles/Events/ProfileChanged.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
|
||||
namespace CustomizePlus.Profiles.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when profile is changed
|
||||
/// </summary>
|
||||
public sealed class ProfileChanged() : EventWrapper<ProfileChanged.Type, Profile?, object?, ProfileChanged.Priority>(nameof(ProfileChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
Created,
|
||||
Deleted,
|
||||
Renamed,
|
||||
Toggled,
|
||||
ChangedCharacterName,
|
||||
AddedTemplate,
|
||||
RemovedTemplate,
|
||||
MovedTemplate,
|
||||
ChangedTemplate,
|
||||
ReloadedAll,
|
||||
WriteProtection,
|
||||
LimitLookupToOwnedChanged,
|
||||
ChangedDefaultProfile,
|
||||
TemporaryProfileAdded,
|
||||
TemporaryProfileDeleted,
|
||||
/*
|
||||
ToggledProfile,
|
||||
AddedTemplate,
|
||||
RemovedTemplate,
|
||||
MovedTemplate,
|
||||
ChangedTemplate*/
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
ProfileFileSystemSelector = -2,
|
||||
TemplateFileSystemSelector = -1,
|
||||
ProfileFileSystem,
|
||||
ArmatureManager,
|
||||
TemplateManager,
|
||||
CustomizePlusIpc
|
||||
}
|
||||
}
|
||||
149
CustomizePlus/Profiles/ProfileFileSystem.cs
Normal file
149
CustomizePlus/Profiles/ProfileFileSystem.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Log;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
|
||||
namespace CustomizePlus.Profiles;
|
||||
|
||||
public class ProfileFileSystem : FileSystem<Profile>, IDisposable, ISavable
|
||||
{
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly ProfileChanged _profileChanged;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ProfileFileSystem(
|
||||
ProfileManager profileManager,
|
||||
SaveService saveService,
|
||||
ProfileChanged profileChanged,
|
||||
MessageService messageService,
|
||||
Logger logger)
|
||||
{
|
||||
_profileManager = profileManager;
|
||||
_saveService = saveService;
|
||||
_profileChanged = profileChanged;
|
||||
_messageService = messageService;
|
||||
_logger = logger;
|
||||
|
||||
_profileChanged.Subscribe(OnProfileChange, ProfileChanged.Priority.ProfileFileSystem);
|
||||
|
||||
Changed += OnChange;
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_profileChanged.Unsubscribe(OnProfileChange);
|
||||
}
|
||||
|
||||
// Search the entire filesystem for the leaf corresponding to a profile.
|
||||
public bool FindLeaf(Profile profile, [NotNullWhen(true)] out Leaf? leaf)
|
||||
{
|
||||
leaf = Root.GetAllDescendants(ISortMode<Profile>.Lexicographical)
|
||||
.OfType<Leaf>()
|
||||
.FirstOrDefault(l => l.Value == profile);
|
||||
return leaf != null;
|
||||
}
|
||||
|
||||
private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? data)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ProfileChanged.Type.Created:
|
||||
var parent = Root;
|
||||
if (data is string path)
|
||||
try
|
||||
{
|
||||
parent = FindOrCreateAllFolders(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_messageService.NotificationMessage(ex, $"Could not move profile to {path} because the folder could not be created.", NotificationType.Error);
|
||||
}
|
||||
|
||||
CreateDuplicateLeaf(parent, profile.Name.Text, profile);
|
||||
|
||||
return;
|
||||
case ProfileChanged.Type.Deleted:
|
||||
if (FindLeaf(profile, out var leaf1))
|
||||
Delete(leaf1);
|
||||
return;
|
||||
case ProfileChanged.Type.ReloadedAll:
|
||||
Reload();
|
||||
return;
|
||||
case ProfileChanged.Type.Renamed when data is string oldName:
|
||||
if (!FindLeaf(profile, out var leaf2))
|
||||
return;
|
||||
|
||||
var old = oldName.FixName();
|
||||
if (old == leaf2.Name || leaf2.Name.IsDuplicateName(out var baseName, out _) && baseName == old)
|
||||
RenameWithDuplicates(leaf2, profile.Name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
if (Load(new FileInfo(_saveService.FileNames.ProfileFileSystem), _profileManager.Profiles, ProfileToIdentifier, ProfileToName))
|
||||
{
|
||||
var shouldReloadAgain = false;
|
||||
|
||||
if (!File.Exists(_saveService.FileNames.ProfileFileSystem))
|
||||
shouldReloadAgain = true;
|
||||
|
||||
_saveService.ImmediateSave(this);
|
||||
|
||||
//this is a workaround for FileSystem's weird behavior where it doesn't load objects into itself if its file does not exist
|
||||
if (shouldReloadAgain)
|
||||
{
|
||||
_logger.Debug("BUG WORKAROUND: reloading profile filesystem again");
|
||||
Reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Reloaded profile filesystem.");
|
||||
}
|
||||
|
||||
private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
|
||||
{
|
||||
if (type != FileSystemChangeType.Reload)
|
||||
_saveService.QueueSave(this);
|
||||
}
|
||||
|
||||
// Used for saving and loading.
|
||||
private static string ProfileToIdentifier(Profile profile)
|
||||
=> profile.UniqueId.ToString();
|
||||
|
||||
private static string ProfileToName(Profile profile)
|
||||
=> profile.Name.Text.FixName();
|
||||
|
||||
private static bool ProfileHasDefaultPath(Profile profile, string fullPath)
|
||||
{
|
||||
var regex = new Regex($@"^{Regex.Escape(ProfileToName(profile))}( \(\d+\))?$");
|
||||
return regex.IsMatch(fullPath);
|
||||
}
|
||||
|
||||
private static (string, bool) SaveProfile(Profile profile, string fullPath)
|
||||
// Only save pairs with non-default paths.
|
||||
=> ProfileHasDefaultPath(profile, fullPath)
|
||||
? (string.Empty, false)
|
||||
: (ProfileToIdentifier(profile), true);
|
||||
|
||||
public string ToFilename(FilenameService fileNames) => fileNames.ProfileFileSystem;
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
SaveToFile(writer, SaveProfile, true);
|
||||
}
|
||||
}
|
||||
618
CustomizePlus/Profiles/ProfileManager.cs
Normal file
618
CustomizePlus/Profiles/ProfileManager.cs
Normal file
@@ -0,0 +1,618 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.GameData.Actors;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
using CustomizePlus.Armatures.Events;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using CustomizePlus.Core.Events;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.GameData.Data;
|
||||
using CustomizePlus.GameData.Services;
|
||||
using CustomizePlus.GameData.Extensions;
|
||||
|
||||
namespace CustomizePlus.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Container class for administrating <see cref="Profile" />s during runtime.
|
||||
/// </summary>
|
||||
public class ProfileManager : IDisposable
|
||||
{
|
||||
private readonly TemplateManager _templateManager;
|
||||
private readonly TemplateEditorManager _templateEditorManager;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly Logger _logger;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly ActorService _actorService;
|
||||
private readonly ProfileChanged _event;
|
||||
private readonly TemplateChanged _templateChangedEvent;
|
||||
private readonly ReloadEvent _reloadEvent;
|
||||
private readonly ArmatureChanged _armatureChangedEvent;
|
||||
|
||||
public readonly List<Profile> Profiles = new();
|
||||
|
||||
public Profile? DefaultProfile { get; private set; }
|
||||
|
||||
public ProfileManager(
|
||||
TemplateManager templateManager,
|
||||
TemplateEditorManager templateEditorManager,
|
||||
SaveService saveService,
|
||||
Logger logger,
|
||||
PluginConfiguration configuration,
|
||||
ActorService actorService,
|
||||
ProfileChanged @event,
|
||||
TemplateChanged templateChangedEvent,
|
||||
ReloadEvent reloadEvent,
|
||||
ArmatureChanged armatureChangedEvent)
|
||||
{
|
||||
_templateManager = templateManager;
|
||||
_templateEditorManager = templateEditorManager;
|
||||
_saveService = saveService;
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_actorService = actorService;
|
||||
_event = @event;
|
||||
_templateChangedEvent = templateChangedEvent;
|
||||
_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.ProfileManager);
|
||||
_reloadEvent = reloadEvent;
|
||||
_reloadEvent.Subscribe(OnReload, ReloadEvent.Priority.ProfileManager);
|
||||
_armatureChangedEvent = armatureChangedEvent;
|
||||
_armatureChangedEvent.Subscribe(OnArmatureChange, ArmatureChanged.Priority.ProfileManager);
|
||||
|
||||
CreateProfileFolder(saveService);
|
||||
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_templateChangedEvent.Unsubscribe(OnTemplateChange);
|
||||
}
|
||||
|
||||
public void LoadProfiles()
|
||||
{
|
||||
_logger.Information("Loading profiles from directory...");
|
||||
|
||||
//todo: hot reload was not tested
|
||||
//save temp profiles
|
||||
var temporaryProfiles = Profiles.Where(x => x.IsTemporary).ToList();
|
||||
|
||||
Profiles.Clear();
|
||||
List<(Profile, string)> invalidNames = new();
|
||||
foreach (var file in _saveService.FileNames.Profiles())
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(file.FullName);
|
||||
var data = JObject.Parse(text);
|
||||
var profile = Profile.Load(_templateManager, data);
|
||||
if (profile.UniqueId.ToString() != Path.GetFileNameWithoutExtension(file.Name))
|
||||
invalidNames.Add((profile, file.FullName));
|
||||
if (Profiles.Any(f => f.UniqueId == profile.UniqueId))
|
||||
throw new Exception($"ID {profile.UniqueId} was not unique.");
|
||||
|
||||
Profiles.Add(profile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Could not load profile, skipped:\n{ex}");
|
||||
//++skipped;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var profile in Profiles)
|
||||
{
|
||||
//This will solve any issues if file on disk was manually edited and we have more than a single active profile
|
||||
if (profile.Enabled)
|
||||
SetEnabled(profile, true, true);
|
||||
|
||||
if (_configuration.DefaultProfile == profile.UniqueId)
|
||||
DefaultProfile = profile;
|
||||
}
|
||||
|
||||
//insert temp profiles back into profile list
|
||||
if (temporaryProfiles.Count > 0)
|
||||
{
|
||||
Profiles.AddRange(temporaryProfiles);
|
||||
Profiles.Sort((x, y) => y.IsTemporary.CompareTo(x.IsTemporary));
|
||||
}
|
||||
|
||||
var failed = MoveInvalidNames(invalidNames);
|
||||
if (invalidNames.Count > 0)
|
||||
_logger.Information(
|
||||
$"Moved {invalidNames.Count - failed} profiles to correct names.{(failed > 0 ? $" Failed to move {failed} profiles to correct names." : string.Empty)}");
|
||||
|
||||
_logger.Information("Directory load complete");
|
||||
_event.Invoke(ProfileChanged.Type.ReloadedAll, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main rendering function, called from rendering hook after calling ArmatureManager.OnRender
|
||||
/// </summary>
|
||||
public void OnRender()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Profile Create(string name, bool handlePath)
|
||||
{
|
||||
var (actualName, path) = NameParsingHelper.ParseName(name, handlePath);
|
||||
var profile = new Profile
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = CreateNewGuid(),
|
||||
Name = actualName
|
||||
};
|
||||
|
||||
Profiles.Add(profile);
|
||||
_logger.Debug($"Added new profile {profile.UniqueId}.");
|
||||
|
||||
_saveService.ImmediateSave(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.Created, profile, path);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new profile by cloning passed profile
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Profile Clone(Profile clone, string name, bool handlePath)
|
||||
{
|
||||
var (actualName, path) = NameParsingHelper.ParseName(name, handlePath);
|
||||
var profile = new Profile(clone)
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
Profiles.Add(profile);
|
||||
_logger.Debug($"Added new profile {profile.UniqueId} by cloning.");
|
||||
|
||||
_saveService.ImmediateSave(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.Created, profile, path);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rename profile
|
||||
/// </summary>
|
||||
public void Rename(Profile profile, string newName)
|
||||
{
|
||||
var oldName = profile.Name.Text;
|
||||
if (oldName == newName)
|
||||
return;
|
||||
|
||||
profile.Name = newName;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Renamed profile {profile.UniqueId}.");
|
||||
_event.Invoke(ProfileChanged.Type.Renamed, profile, oldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change character name for profile
|
||||
/// </summary>
|
||||
public void ChangeCharacterName(Profile profile, string newName)
|
||||
{
|
||||
var oldName = profile.CharacterName.Text;
|
||||
if (oldName == newName)
|
||||
return;
|
||||
|
||||
profile.CharacterName = newName;
|
||||
|
||||
//Called so all other active profiles for new character name get disabled
|
||||
//saving is performed there
|
||||
SetEnabled(profile, profile.Enabled, true);
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Changed character name for profile {profile.UniqueId}.");
|
||||
_event.Invoke(ProfileChanged.Type.ChangedCharacterName, profile, oldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete profile
|
||||
/// </summary>
|
||||
/// <param name="profile"></param>
|
||||
public void Delete(Profile profile)
|
||||
{
|
||||
Profiles.Remove(profile);
|
||||
_saveService.ImmediateDelete(profile);
|
||||
_event.Invoke(ProfileChanged.Type.Deleted, profile, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set write protection state for profile
|
||||
/// </summary>
|
||||
public void SetWriteProtection(Profile profile, bool value)
|
||||
{
|
||||
if (profile.IsWriteProtected == value)
|
||||
return;
|
||||
|
||||
profile.IsWriteProtected = value;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Set profile {profile.UniqueId} to {(value ? string.Empty : "no longer be ")} write-protected.");
|
||||
_event.Invoke(ProfileChanged.Type.WriteProtection, profile, value);
|
||||
}
|
||||
|
||||
public void SetEnabled(Profile profile, bool value, bool force = false)
|
||||
{
|
||||
if (profile.Enabled == value && !force)
|
||||
return;
|
||||
|
||||
var oldValue = profile.Enabled;
|
||||
|
||||
if (value)
|
||||
{
|
||||
_logger.Debug($"Setting {profile} as enabled...");
|
||||
|
||||
foreach (var otherProfile in Profiles
|
||||
.Where(x => x.CharacterName == profile.CharacterName && x != profile && x.Enabled && !x.IsTemporary))
|
||||
{
|
||||
_logger.Debug($"\t-> {otherProfile} disabled");
|
||||
SetEnabled(otherProfile, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldValue != value)
|
||||
{
|
||||
profile.Enabled = value;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.Toggled, profile, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLimitLookupToOwned(Profile profile, bool value)
|
||||
{
|
||||
if (profile.LimitLookupToOwnedObjects != value)
|
||||
{
|
||||
profile.LimitLookupToOwnedObjects = value;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.LimitLookupToOwnedChanged, profile, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteTemplate(Profile profile, int templateIndex)
|
||||
{
|
||||
_logger.Debug($"Deleting template #{templateIndex} from {profile}...");
|
||||
|
||||
var template = profile.Templates[templateIndex];
|
||||
profile.Templates.RemoveAt(templateIndex);
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.RemovedTemplate, profile, template);
|
||||
}
|
||||
|
||||
public void AddTemplate(Profile profile, Template template)
|
||||
{
|
||||
if (profile.Templates.Contains(template))
|
||||
return;
|
||||
|
||||
profile.Templates.Add(template);
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Added template: {template.UniqueId} to {profile.UniqueId}");
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.AddedTemplate, profile, template);
|
||||
}
|
||||
|
||||
public void ChangeTemplate(Profile profile, int index, Template newTemplate)
|
||||
{
|
||||
if (index >= profile.Templates.Count || index < 0)
|
||||
return;
|
||||
|
||||
if (profile.Templates[index] == newTemplate)
|
||||
return;
|
||||
|
||||
var oldTemplate = profile.Templates[index];
|
||||
profile.Templates[index] = newTemplate;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Changed template on profile {profile.UniqueId} from {oldTemplate.UniqueId} to {newTemplate.UniqueId}");
|
||||
_event.Invoke(ProfileChanged.Type.ChangedTemplate, profile, (index, oldTemplate, newTemplate));
|
||||
}
|
||||
|
||||
public void MoveTemplate(Profile profile, int fromIndex, int toIndex)
|
||||
{
|
||||
if (!profile.Templates.Move(fromIndex, toIndex))
|
||||
return;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Moved template {fromIndex + 1} to position {toIndex + 1}.");
|
||||
_event.Invoke(ProfileChanged.Type.MovedTemplate, profile, (fromIndex, toIndex));
|
||||
}
|
||||
|
||||
public void SetDefaultProfile(Profile? profile)
|
||||
{
|
||||
if (profile == null)
|
||||
{
|
||||
if (DefaultProfile == null)
|
||||
return;
|
||||
}
|
||||
else if (!Profiles.Contains(profile))
|
||||
return;
|
||||
|
||||
var previousProfile = DefaultProfile;
|
||||
|
||||
DefaultProfile = profile;
|
||||
_configuration.DefaultProfile = profile?.UniqueId ?? Guid.Empty;
|
||||
_configuration.Save();
|
||||
|
||||
_logger.Debug($"Set profile {profile?.Incognito ?? "no profile"} as default");
|
||||
_event.Invoke(ProfileChanged.Type.ChangedDefaultProfile, profile, previousProfile);
|
||||
}
|
||||
|
||||
public void AddTemporaryProfile(Profile profile, Actor actor/*, Template template*/)
|
||||
{
|
||||
if (!actor.Identifier(_actorService.AwaitedService, out var identifier))
|
||||
return;
|
||||
|
||||
profile.Enabled = true;
|
||||
profile.IsTemporary = true;
|
||||
profile.TemporaryActor = identifier;
|
||||
profile.CharacterName = identifier.ToNameWithoutOwnerName();
|
||||
profile.LimitLookupToOwnedObjects = false;
|
||||
|
||||
var existingProfile = Profiles.FirstOrDefault(x => x.CharacterName.Lower == profile.CharacterName.Lower && x.IsTemporary);
|
||||
if (existingProfile != null)
|
||||
{
|
||||
_logger.Debug($"Temporary profile for {existingProfile.CharacterName} already exists, removing...");
|
||||
Profiles.Remove(existingProfile);
|
||||
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, existingProfile, null);
|
||||
}
|
||||
|
||||
Profiles.Add(profile);
|
||||
|
||||
//Make sure temporary profiles come first, so they are returned by all other methods first
|
||||
Profiles.Sort((x, y) => y.IsTemporary.CompareTo(x.IsTemporary));
|
||||
|
||||
_logger.Debug($"Added temporary profile for {identifier}");
|
||||
_event.Invoke(ProfileChanged.Type.TemporaryProfileAdded, profile, null);
|
||||
}
|
||||
|
||||
public void RemoveTemporaryProfile(Profile profile)
|
||||
{
|
||||
if (!Profiles.Remove(profile))
|
||||
return;
|
||||
|
||||
_logger.Debug($"Removed temporary profile for {profile.CharacterName}");
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, profile, null);
|
||||
}
|
||||
|
||||
public void RemoveTemporaryProfile(Actor actor)
|
||||
{
|
||||
if (!actor.Identifier(_actorService.AwaitedService, out var identifier))
|
||||
return;
|
||||
|
||||
var profile = Profiles.FirstOrDefault(x => x.TemporaryActor == identifier && x.IsTemporary);
|
||||
if (profile == null)
|
||||
return;
|
||||
|
||||
RemoveTemporaryProfile(profile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return profile by character name, does not return temporary profiles
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="enabledOnly"></param>
|
||||
/// <returns></returns>
|
||||
public Profile? GetProfileByCharacterName(string name, bool enabledOnly = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return null;
|
||||
|
||||
var query = Profiles.Where(x => x.CharacterName == name);
|
||||
if (enabledOnly)
|
||||
query = query.Where(x => x.Enabled);
|
||||
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
//todo: replace with dictionary
|
||||
/// <summary>
|
||||
/// Returns all enabled profiles which might apply to the given object, prioritizing temporary profiles and editor profile.
|
||||
/// </summary>
|
||||
public IEnumerable<Profile> GetEnabledProfilesByActor(ActorIdentifier actorIdentifier)
|
||||
{
|
||||
//performance: using textual override for ProfileAppliesTo here to not call
|
||||
//GetGameObjectName every time we are trying to check object against profiles
|
||||
|
||||
if (actorIdentifier.Type == IdentifierType.Special)
|
||||
actorIdentifier = actorIdentifier.GetTrueActorForSpecialType();
|
||||
|
||||
if (!actorIdentifier.IsValid)
|
||||
yield break;
|
||||
|
||||
var name = actorIdentifier.ToNameWithoutOwnerName();
|
||||
|
||||
if (name.IsNullOrWhitespace())
|
||||
yield break;
|
||||
|
||||
if (_templateEditorManager.IsEditorActive && _templateEditorManager.EditorProfile.Enabled)
|
||||
{
|
||||
if (ProfileAppliesTo(_templateEditorManager.EditorProfile, name))
|
||||
{
|
||||
yield return _templateEditorManager.EditorProfile;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var profile in Profiles)
|
||||
{
|
||||
if (ProfileAppliesTo(profile, name) && profile.Enabled)
|
||||
yield return profile;
|
||||
}
|
||||
|
||||
if (DefaultProfile != null &&
|
||||
DefaultProfile.Enabled &&
|
||||
(actorIdentifier.Type == IdentifierType.Player || actorIdentifier.Type == IdentifierType.Retainer))
|
||||
yield return DefaultProfile;
|
||||
}
|
||||
|
||||
public IEnumerable<Profile> GetProfilesUsingTemplate(Template template)
|
||||
{
|
||||
if (template == null)
|
||||
yield break;
|
||||
|
||||
foreach (var profile in Profiles)
|
||||
if (profile.Templates.Contains(template))
|
||||
yield return profile;
|
||||
|
||||
if (_templateEditorManager.EditorProfile.Templates.Contains(template))
|
||||
yield return _templateEditorManager.EditorProfile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not profile applies to the object with the indicated name.
|
||||
/// </summary>
|
||||
public bool ProfileAppliesTo(Profile profile, string objectName) => !string.IsNullOrWhiteSpace(objectName) && objectName == profile.CharacterName.Text;
|
||||
|
||||
private void SaveProfile(Profile profile)
|
||||
{
|
||||
profile.ModifiedDate = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(profile);
|
||||
}
|
||||
|
||||
private void OnTemplateChange(TemplateChanged.Type type, Template? template, object? arg3)
|
||||
{
|
||||
if (type is not TemplateChanged.Type.Deleted)
|
||||
return;
|
||||
|
||||
foreach (var profile in Profiles)
|
||||
{
|
||||
for (var i = 0; i < profile.Templates.Count; ++i)
|
||||
{
|
||||
if (profile.Templates[i] != template)
|
||||
continue;
|
||||
|
||||
profile.Templates.RemoveAt(i--);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.RemovedTemplate, profile, template);
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Removed template {template.UniqueId} from {profile.UniqueId} because template was deleted");
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private void OnReload(ReloadEvent.Type type)
|
||||
{
|
||||
if (type != ReloadEvent.Type.ReloadProfiles &&
|
||||
type != ReloadEvent.Type.ReloadAll)
|
||||
return;
|
||||
|
||||
_logger.Debug("Reload event received");
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
|
||||
private void OnArmatureChange(ArmatureChanged.Type type, Armature? armature, object? arg3)
|
||||
{
|
||||
if (type == ArmatureChanged.Type.Deleted)
|
||||
{
|
||||
//hack: sending TemporaryProfileDeleted will result in OnArmatureChange being sent
|
||||
//so we need to make sure that we do not end up with endless loop here
|
||||
//the whole reason DeletionReason exists is this
|
||||
if ((ArmatureChanged.DeletionReason)arg3 != ArmatureChanged.DeletionReason.Gone)
|
||||
return;
|
||||
|
||||
var profile = armature!.Profile;
|
||||
|
||||
//todo: TemporaryProfileDeleted ends up calling this again, fix this.
|
||||
//Profiles.Remove check won't allow for infinite loop but this isn't good anyway
|
||||
if (!profile.IsTemporary || !Profiles.Remove(profile))
|
||||
return;
|
||||
|
||||
_logger.Debug($"ProfileManager.OnArmatureChange: Removed unused temporary profile for {profile.CharacterName}");
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, profile, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateProfileFolder(SaveService service)
|
||||
{
|
||||
var ret = service.FileNames.ProfileDirectory;
|
||||
if (Directory.Exists(ret))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Error($"Could not create profile directory {ret}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Move all files that were discovered to have names not corresponding to their identifier to correct names, if possible. </summary>
|
||||
/// <returns>The number of files that could not be moved.</returns>
|
||||
private int MoveInvalidNames(IEnumerable<(Profile, string)> invalidNames)
|
||||
{
|
||||
var failed = 0;
|
||||
foreach (var (profile, name) in invalidNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var correctName = _saveService.FileNames.ProfileFile(profile);
|
||||
File.Move(name, correctName, false);
|
||||
_logger.Information($"Moved invalid profile file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
++failed;
|
||||
_logger.Error($"Failed to move invalid profile file from {Path.GetFileName(name)}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
return failed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new guid until we find one which isn't used by existing profile
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Guid CreateNewGuid()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
if (Profiles.All(d => d.UniqueId != guid))
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user