Code commit

This commit is contained in:
RisaDev
2024-01-06 01:21:41 +03:00
parent a7d7297c59
commit a486dd2c96
90 changed files with 11576 additions and 0 deletions

View 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
}

View 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
}
}

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

View 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;
}
}
}