Code commit
This commit is contained in:
128
CustomizePlus/Templates/Data/Template.cs
Normal file
128
CustomizePlus/Templates/Data/Template.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Core.Extensions;
|
||||
using CustomizePlus.Core.Services;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace CustomizePlus.Templates.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates the user-controlled aspects of a template, ie all of
|
||||
/// the information that gets saved to disk by the plugin.
|
||||
/// </summary>
|
||||
public sealed class Template : ISavable
|
||||
{
|
||||
public LowerString Name { get; internal set; } = "Template";
|
||||
|
||||
public int Version { get; internal set; } = Constants.ConfigurationVersion;
|
||||
public DateTimeOffset CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTimeOffset ModifiedDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
public Guid UniqueId { get; internal set; } = Guid.NewGuid();
|
||||
|
||||
public bool IsWriteProtected { get; internal set; }
|
||||
|
||||
public string Incognito
|
||||
=> UniqueId.ToString()[..8];
|
||||
|
||||
public Dictionary<string, BoneTransform> Bones { get; init; } = new();
|
||||
|
||||
public Template()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new template based on bone data from another one
|
||||
/// </summary>
|
||||
public Template(Template original) : this()
|
||||
{
|
||||
foreach (var kvp in original.Bones)
|
||||
{
|
||||
Bones[kvp.Key] = new BoneTransform();
|
||||
Bones[kvp.Key].UpdateToMatch(kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Template '{Name.Text.Incognify()}' with {Bones.Count} bone edits [{UniqueId}]";
|
||||
}
|
||||
|
||||
#region Serialization
|
||||
|
||||
public new JObject JsonSerialize()
|
||||
{
|
||||
var ret = new JObject()
|
||||
{
|
||||
["Version"] = Version,
|
||||
["UniqueId"] = UniqueId,
|
||||
["CreationDate"] = CreationDate,
|
||||
["ModifiedDate"] = ModifiedDate,
|
||||
["Name"] = Name.Text,
|
||||
["Bones"] = JObject.FromObject(Bones),
|
||||
["IsWriteProtected"] = IsWriteProtected
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deserialization
|
||||
|
||||
public static Template Load(JObject obj)
|
||||
{
|
||||
var version = obj["Version"]?.ToObject<int>() ?? 0;
|
||||
return version switch
|
||||
{
|
||||
//Did not exist before v4
|
||||
4 => LoadV4(obj),
|
||||
_ => throw new Exception("The design to be loaded has no valid Version."),
|
||||
};
|
||||
}
|
||||
|
||||
private static Template LoadV4(JObject obj)
|
||||
{
|
||||
var creationDate = obj["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
|
||||
|
||||
var template = new Template()
|
||||
{
|
||||
CreationDate = creationDate,
|
||||
UniqueId = obj["UniqueId"]?.ToObject<Guid>() ?? throw new ArgumentNullException("UniqueId"),
|
||||
Name = new LowerString(obj["Name"]?.ToObject<string>() ?? throw new ArgumentNullException("Name")),
|
||||
ModifiedDate = obj["ModifiedDate"]?.ToObject<DateTimeOffset>() ?? creationDate,
|
||||
Bones = obj["Bones"]?.ToObject<Dictionary<string, BoneTransform>>() ?? throw new ArgumentNullException("Bones"),
|
||||
IsWriteProtected = obj["IsWriteProtected"]?.ToObject<bool>() ?? false
|
||||
};
|
||||
if (template.ModifiedDate < creationDate)
|
||||
template.ModifiedDate = creationDate;
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISavable
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.TemplateFile(this);
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var j = new JsonTextWriter(writer)
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
var obj = JsonSerialize();
|
||||
obj.WriteTo(j);
|
||||
}
|
||||
|
||||
public string LogName(string fileName)
|
||||
=> Path.GetFileNameWithoutExtension(fileName);
|
||||
|
||||
#endregion
|
||||
}
|
||||
36
CustomizePlus/Templates/Events/TemplateChanged.cs
Normal file
36
CustomizePlus/Templates/Events/TemplateChanged.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using CustomizePlus.Templates.Data;
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
|
||||
namespace CustomizePlus.Templates.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when Template is changed
|
||||
/// </summary>
|
||||
public class TemplateChanged() : EventWrapper<TemplateChanged.Type, Template?, object?, TemplateChanged.Priority>(nameof(TemplateChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
Created,
|
||||
Deleted,
|
||||
Renamed,
|
||||
NewBone,
|
||||
UpdatedBone,
|
||||
DeletedBone,
|
||||
EditorEnabled,
|
||||
EditorDisabled,
|
||||
EditorCharacterChanged,
|
||||
ReloadedAll,
|
||||
WriteProtection
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
TemplateCombo = -2,
|
||||
TemplateFileSystemSelector = -1,
|
||||
TemplateFileSystem,
|
||||
ArmatureManager,
|
||||
ProfileManager,
|
||||
CustomizePlusIpc
|
||||
}
|
||||
}
|
||||
288
CustomizePlus/Templates/TemplateEditorManager.cs
Normal file
288
CustomizePlus/Templates/TemplateEditorManager.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Game.Events;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using OtterGui.Log;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace CustomizePlus.Templates;
|
||||
|
||||
public class TemplateEditorManager : IDisposable
|
||||
{
|
||||
private readonly TemplateChanged _event;
|
||||
private readonly GPoseStateChanged _gposeStateChanged;
|
||||
private readonly Logger _logger;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
private readonly TemplateManager _templateManager;
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the original template which is currently being edited, should not be edited!
|
||||
/// </summary>
|
||||
private Template _currentlyEditedTemplateOriginal;
|
||||
|
||||
/// <summary>
|
||||
/// Internal profile for the editor
|
||||
/// </summary>
|
||||
public Profile EditorProfile { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Original ID of the template which is currently being edited
|
||||
/// </summary>
|
||||
public Guid CurrentlyEditedTemplateId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A copy of currently edited template, all changes must be done on this template
|
||||
/// </summary>
|
||||
public Template? CurrentlyEditedTemplate { get; private set; }
|
||||
|
||||
public bool IsEditorActive { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is editor currently paused? Happens automatically when editor is not compatible with the current game state.
|
||||
/// Keeps editor state frozen and prevents any changes to it, also sets editor profile as disabled.
|
||||
/// </summary>
|
||||
public bool IsEditorPaused { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if there are any changes in current editing session or not
|
||||
/// </summary>
|
||||
public bool HasChanges { get; private set; }
|
||||
|
||||
public bool IsKeepOnlyEditorProfileActive { get; set; } //todo
|
||||
|
||||
public TemplateEditorManager(
|
||||
TemplateChanged @event,
|
||||
GPoseStateChanged gposeStateChanged,
|
||||
Logger logger,
|
||||
TemplateManager templateManager,
|
||||
GameObjectService gameObjectService)
|
||||
{
|
||||
_event = @event;
|
||||
_gposeStateChanged = gposeStateChanged;
|
||||
_logger = logger;
|
||||
_templateManager = templateManager;
|
||||
_gameObjectService = gameObjectService;
|
||||
|
||||
_gposeStateChanged.Subscribe(OnGPoseStateChanged, GPoseStateChanged.Priority.TemplateEditorManager);
|
||||
|
||||
EditorProfile = new Profile() { Templates = new List<Template>(), Enabled = false, Name = "Template editor profile" };
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gposeStateChanged.Unsubscribe(OnGPoseStateChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn on editing of a specific template. If character name not set will default to local player.
|
||||
/// </summary>
|
||||
internal bool EnableEditor(Template template, string? characterName = null) //todo: editor is borked
|
||||
{
|
||||
if (IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
_logger.Debug($"Enabling editor profile for {template.Name} via character {characterName}");
|
||||
|
||||
CurrentlyEditedTemplateId = template.UniqueId;
|
||||
_currentlyEditedTemplateOriginal = template;
|
||||
CurrentlyEditedTemplate = new Template(template)
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = Guid.NewGuid(),
|
||||
Name = "Template editor temporary template"
|
||||
};
|
||||
|
||||
if (characterName != null)
|
||||
EditorProfile.CharacterName = characterName;
|
||||
else //safeguard
|
||||
EditorProfile.CharacterName = _gameObjectService.GetCurrentPlayerName();
|
||||
|
||||
EditorProfile.Templates.Clear(); //safeguard
|
||||
EditorProfile.Templates.Add(CurrentlyEditedTemplate);
|
||||
EditorProfile.Enabled = true;
|
||||
HasChanges = false;
|
||||
IsEditorActive = true;
|
||||
|
||||
_event.Invoke(TemplateChanged.Type.EditorEnabled, template, characterName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn off editing of a specific template
|
||||
/// </summary>
|
||||
internal bool DisableEditor()
|
||||
{
|
||||
if (!IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
_logger.Debug($"Disabling editor profile");
|
||||
|
||||
string characterName = EditorProfile.CharacterName;
|
||||
|
||||
CurrentlyEditedTemplateId = Guid.Empty;
|
||||
CurrentlyEditedTemplate = null;
|
||||
EditorProfile.Enabled = false;
|
||||
EditorProfile.CharacterName = "";
|
||||
EditorProfile.Templates.Clear();
|
||||
IsEditorActive = false;
|
||||
HasChanges = false;
|
||||
|
||||
_event.Invoke(TemplateChanged.Type.EditorDisabled, null, characterName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SaveChanges(bool asCopy = false)
|
||||
{
|
||||
var targetTemplate = _templateManager.GetTemplate(CurrentlyEditedTemplateId);
|
||||
if (targetTemplate == null)
|
||||
throw new Exception($"Fatal editor error: Template with ID {CurrentlyEditedTemplateId} not found in template manager");
|
||||
|
||||
if (asCopy)
|
||||
targetTemplate = _templateManager.Clone(targetTemplate, $"{targetTemplate.Name} - Copy {Guid.NewGuid().ToString().Substring(0, 4)}", false);
|
||||
|
||||
_templateManager.ApplyBoneChangesAndSave(targetTemplate, CurrentlyEditedTemplate!);
|
||||
}
|
||||
|
||||
public bool ChangeEditorCharacter(string characterName)
|
||||
{
|
||||
if (!IsEditorActive || EditorProfile.CharacterName == characterName || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
_logger.Debug($"Changing character name for editor profile from {EditorProfile.CharacterName} to {characterName}");
|
||||
|
||||
EditorProfile.CharacterName = characterName;
|
||||
_event.Invoke(TemplateChanged.Type.EditorCharacterChanged, CurrentlyEditedTemplate, (characterName, EditorProfile));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets changes in currently edited template to default values
|
||||
/// </summary>
|
||||
public bool ResetBoneAttributeChanges(string boneName, BoneAttribute attribute)
|
||||
{
|
||||
if (!IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
if (!CurrentlyEditedTemplate!.Bones.ContainsKey(boneName))
|
||||
return false;
|
||||
|
||||
var resetValue = GetResetValueForAttribute(attribute);
|
||||
|
||||
switch (attribute)
|
||||
{
|
||||
case BoneAttribute.Position:
|
||||
if (resetValue == CurrentlyEditedTemplate!.Bones[boneName].Translation)
|
||||
return false;
|
||||
break;
|
||||
case BoneAttribute.Rotation:
|
||||
if (resetValue == CurrentlyEditedTemplate!.Bones[boneName].Rotation)
|
||||
return false;
|
||||
break;
|
||||
case BoneAttribute.Scale:
|
||||
if (resetValue == CurrentlyEditedTemplate!.Bones[boneName].Scaling)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
CurrentlyEditedTemplate!.Bones[boneName].UpdateAttribute(attribute, resetValue);
|
||||
|
||||
if (!HasChanges)
|
||||
HasChanges = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverts changes in currently edited template to values set in saved copy of the template.
|
||||
/// Resets to default value if saved copy doesn't have that bone edited
|
||||
/// </summary>
|
||||
public bool RevertBoneAttributeChanges(string boneName, BoneAttribute attribute)
|
||||
{
|
||||
if (!IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
if (!CurrentlyEditedTemplate!.Bones.ContainsKey(boneName))
|
||||
return false;
|
||||
|
||||
Vector3? originalValue = null!;
|
||||
|
||||
if (_currentlyEditedTemplateOriginal.Bones.ContainsKey(boneName))
|
||||
{
|
||||
switch (attribute)
|
||||
{
|
||||
case BoneAttribute.Position:
|
||||
originalValue = _currentlyEditedTemplateOriginal.Bones[boneName].Translation;
|
||||
if (originalValue == CurrentlyEditedTemplate!.Bones[boneName].Translation)
|
||||
return false;
|
||||
break;
|
||||
case BoneAttribute.Rotation:
|
||||
originalValue = _currentlyEditedTemplateOriginal.Bones[boneName].Rotation;
|
||||
if (originalValue == CurrentlyEditedTemplate!.Bones[boneName].Rotation)
|
||||
return false;
|
||||
break;
|
||||
case BoneAttribute.Scale:
|
||||
originalValue = _currentlyEditedTemplateOriginal.Bones[boneName].Scaling;
|
||||
if (originalValue == CurrentlyEditedTemplate!.Bones[boneName].Scaling)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
originalValue = GetResetValueForAttribute(attribute);
|
||||
|
||||
CurrentlyEditedTemplate!.Bones[boneName].UpdateAttribute(attribute, originalValue.Value);
|
||||
|
||||
if (!HasChanges)
|
||||
HasChanges = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ModifyBoneTransform(string boneName, BoneTransform transform)
|
||||
{
|
||||
if (!IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
if (!_templateManager.ModifyBoneTransform(CurrentlyEditedTemplate!, boneName, transform))
|
||||
return false;
|
||||
|
||||
if (!HasChanges)
|
||||
HasChanges = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector3 GetResetValueForAttribute(BoneAttribute attribute)
|
||||
{
|
||||
switch (attribute)
|
||||
{
|
||||
case BoneAttribute.Scale:
|
||||
return Vector3.One;
|
||||
default:
|
||||
return Vector3.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGPoseStateChanged(GPoseStateChanged.Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case GPoseStateChanged.Type.Entered:
|
||||
IsEditorPaused = true;
|
||||
EditorProfile.Enabled = false;
|
||||
break;
|
||||
case GPoseStateChanged.Type.Exited:
|
||||
EditorProfile.Enabled = true;
|
||||
IsEditorPaused = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
150
CustomizePlus/Templates/TemplateFileSystem.cs
Normal file
150
CustomizePlus/Templates/TemplateFileSystem.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Log;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Templates.Data;
|
||||
|
||||
namespace CustomizePlus.Templates;
|
||||
|
||||
//Adapted from glamourer source code
|
||||
public sealed class TemplateFileSystem : FileSystem<Template>, IDisposable, ISavable
|
||||
{
|
||||
private readonly TemplateManager _templateManager;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly TemplateChanged _templateChanged;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public TemplateFileSystem(
|
||||
TemplateManager templateManager,
|
||||
SaveService saveService,
|
||||
TemplateChanged templateChanged,
|
||||
MessageService messageService,
|
||||
Logger logger)
|
||||
{
|
||||
_templateManager = templateManager;
|
||||
_saveService = saveService;
|
||||
_templateChanged = templateChanged;
|
||||
_messageService = messageService;
|
||||
_logger = logger;
|
||||
|
||||
_templateChanged.Subscribe(OnTemplateChange, TemplateChanged.Priority.TemplateFileSystem);
|
||||
|
||||
Changed += OnChange;
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_templateChanged.Unsubscribe(OnTemplateChange);
|
||||
}
|
||||
|
||||
// Search the entire filesystem for the leaf corresponding to a template.
|
||||
public bool FindLeaf(Template template, [NotNullWhen(true)] out Leaf? leaf)
|
||||
{
|
||||
leaf = Root.GetAllDescendants(ISortMode<Template>.Lexicographical)
|
||||
.OfType<Leaf>()
|
||||
.FirstOrDefault(l => l.Value == template);
|
||||
return leaf != null;
|
||||
}
|
||||
|
||||
private void OnTemplateChange(TemplateChanged.Type type, Template? template, object? data)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TemplateChanged.Type.Created:
|
||||
var parent = Root;
|
||||
if (data is string path)
|
||||
try
|
||||
{
|
||||
parent = FindOrCreateAllFolders(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_messageService.NotificationMessage(ex, $"Could not move template to {path} because the folder could not be created.", NotificationType.Error);
|
||||
}
|
||||
|
||||
CreateDuplicateLeaf(parent, template.Name.Text, template);
|
||||
|
||||
return;
|
||||
case TemplateChanged.Type.Deleted:
|
||||
if (FindLeaf(template, out var leaf1))
|
||||
Delete(leaf1);
|
||||
return;
|
||||
case TemplateChanged.Type.ReloadedAll:
|
||||
Reload();
|
||||
return;
|
||||
case TemplateChanged.Type.Renamed when data is string oldName:
|
||||
if (!FindLeaf(template, out var leaf2))
|
||||
return;
|
||||
|
||||
var old = oldName.FixName();
|
||||
if (old == leaf2.Name || leaf2.Name.IsDuplicateName(out var baseName, out _) && baseName == old)
|
||||
RenameWithDuplicates(leaf2, template.Name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
if (Load(new FileInfo(_saveService.FileNames.TemplateFileSystem), _templateManager.Templates, TemplateToIdentifier, TemplateToName))
|
||||
{
|
||||
var shouldReloadAgain = false;
|
||||
|
||||
if (!File.Exists(_saveService.FileNames.TemplateFileSystem))
|
||||
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 template filesystem again");
|
||||
Reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Reloaded template 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 TemplateToIdentifier(Template template)
|
||||
=> template.UniqueId.ToString();
|
||||
|
||||
private static string TemplateToName(Template template)
|
||||
=> template.Name.Text.FixName();
|
||||
|
||||
private static bool TemplateHasDefaultPath(Template template, string fullPath)
|
||||
{
|
||||
var regex = new Regex($@"^{Regex.Escape(TemplateToName(template))}( \(\d+\))?$");
|
||||
return regex.IsMatch(fullPath);
|
||||
}
|
||||
|
||||
private static (string, bool) SaveTemplate(Template template, string fullPath)
|
||||
// Only save pairs with non-default paths.
|
||||
=> TemplateHasDefaultPath(template, fullPath)
|
||||
? (string.Empty, false)
|
||||
: (TemplateToIdentifier(template), true);
|
||||
|
||||
public string ToFilename(FilenameService fileNames) => fileNames.TemplateFileSystem;
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
SaveToFile(writer, SaveTemplate, true);
|
||||
}
|
||||
}
|
||||
332
CustomizePlus/Templates/TemplateManager.cs
Normal file
332
CustomizePlus/Templates/TemplateManager.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Core.Events;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Log;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace CustomizePlus.Templates;
|
||||
|
||||
public class TemplateManager
|
||||
{
|
||||
private readonly SaveService _saveService;
|
||||
private readonly Logger _logger;
|
||||
private readonly TemplateChanged _event;
|
||||
private readonly ReloadEvent _reloadEvent;
|
||||
|
||||
private readonly List<Template> _templates = new();
|
||||
|
||||
public IReadOnlyList<Template> Templates
|
||||
=> _templates;
|
||||
|
||||
public TemplateManager(
|
||||
SaveService saveService,
|
||||
Logger logger,
|
||||
TemplateChanged @event,
|
||||
ReloadEvent reloadEvent)
|
||||
{
|
||||
_saveService = saveService;
|
||||
_logger = logger;
|
||||
_event = @event;
|
||||
_reloadEvent = reloadEvent;
|
||||
_reloadEvent.Subscribe(OnReload, ReloadEvent.Priority.TemplateManager);
|
||||
|
||||
CreateTemplateFolder(saveService);
|
||||
LoadTemplates();
|
||||
}
|
||||
|
||||
public Template? GetTemplate(Guid templateId) => _templates.FirstOrDefault(d => d.UniqueId == templateId);
|
||||
|
||||
public void LoadTemplates()
|
||||
{
|
||||
_logger.Information("Loading templates from directory...");
|
||||
|
||||
_templates.Clear();
|
||||
List<(Template, string)> invalidNames = new();
|
||||
foreach (var file in _saveService.FileNames.Templates())
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(file.FullName);
|
||||
var data = JObject.Parse(text);
|
||||
var template = Template.Load(data);
|
||||
if (template.UniqueId.ToString() != Path.GetFileNameWithoutExtension(file.Name))
|
||||
invalidNames.Add((template, file.FullName));
|
||||
if (_templates.Any(f => f.UniqueId == template.UniqueId))
|
||||
throw new Exception($"ID {template.UniqueId} was not unique.");
|
||||
|
||||
PruneIdempotentTransforms(template);
|
||||
|
||||
_templates.Add(template);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Could not load template, skipped:\n{ex}");
|
||||
//++skipped;
|
||||
}
|
||||
}
|
||||
|
||||
var failed = MoveInvalidNames(invalidNames);
|
||||
if (invalidNames.Count > 0)
|
||||
_logger.Information(
|
||||
$"Moved {invalidNames.Count - failed} templates to correct names.{(failed > 0 ? $" Failed to move {failed} templates to correct names." : string.Empty)}");
|
||||
|
||||
_logger.Information("Directory load complete");
|
||||
_event.Invoke(TemplateChanged.Type.ReloadedAll, null, null);
|
||||
}
|
||||
|
||||
public Template Create(string name, Dictionary<string, BoneTransform>? bones, bool handlePath)
|
||||
{
|
||||
var (actualName, path) = NameParsingHelper.ParseName(name, handlePath);
|
||||
var template = new Template
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
Bones = bones != null && bones.Count > 0 ? new Dictionary<string, BoneTransform>(bones) : new()
|
||||
};
|
||||
|
||||
if (template.Bones.Count > 0)
|
||||
PruneIdempotentTransforms(template);
|
||||
|
||||
_templates.Add(template);
|
||||
_logger.Debug($"Added new template {template.UniqueId}.");
|
||||
|
||||
_saveService.ImmediateSave(template);
|
||||
|
||||
_event.Invoke(TemplateChanged.Type.Created, template, path);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
public Template Create(string name, bool handlePath)
|
||||
{
|
||||
return Create(name, null, handlePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new template by cloning passed template
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Template Clone(Template clone, string name, bool handlePath)
|
||||
{
|
||||
var (actualName, path) = NameParsingHelper.ParseName(name, handlePath);
|
||||
var template = new Template(clone)
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = CreateNewGuid(),
|
||||
Name = actualName
|
||||
};
|
||||
|
||||
_templates.Add(template);
|
||||
_logger.Debug($"Added new template {template.UniqueId} by cloning.");
|
||||
|
||||
_saveService.ImmediateSave(template);
|
||||
|
||||
_event.Invoke(TemplateChanged.Type.Created, template, path);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rename template
|
||||
/// </summary>
|
||||
public void Rename(Template template, string newName)
|
||||
{
|
||||
var oldName = template.Name.Text;
|
||||
if (oldName == newName)
|
||||
return;
|
||||
|
||||
template.Name = newName;
|
||||
|
||||
SaveTemplate(template);
|
||||
|
||||
_logger.Debug($"Renamed template {template.UniqueId}.");
|
||||
_event.Invoke(TemplateChanged.Type.Renamed, template, oldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete template
|
||||
/// </summary>
|
||||
/// <param name="template"></param>
|
||||
public void Delete(Template template)
|
||||
{
|
||||
_templates.Remove(template);
|
||||
_saveService.ImmediateDelete(template);
|
||||
_event.Invoke(TemplateChanged.Type.Deleted, template, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set write protection state for template
|
||||
/// </summary>
|
||||
public void SetWriteProtection(Template template, bool value)
|
||||
{
|
||||
if (template.IsWriteProtected == value)
|
||||
return;
|
||||
|
||||
template.IsWriteProtected = value;
|
||||
|
||||
SaveTemplate(template);
|
||||
|
||||
_logger.Debug($"Set template {template.UniqueId} to {(value ? string.Empty : "no longer be ")} write-protected.");
|
||||
_event.Invoke(TemplateChanged.Type.WriteProtection, template, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy bone data from source template to target template and queue a save for target template
|
||||
/// </summary>
|
||||
/// <param name="targetTemplate"></param>
|
||||
/// <param name="sourceTemplate"></param>
|
||||
public void ApplyBoneChangesAndSave(Template targetTemplate, Template sourceTemplate)
|
||||
{
|
||||
_logger.Debug($"Copying bones from {sourceTemplate.Name} to {targetTemplate.Name}");
|
||||
var deletedBones = targetTemplate.Bones.Keys.Except(sourceTemplate.Bones.Keys).ToList();
|
||||
foreach (var kvPair in sourceTemplate.Bones)
|
||||
{
|
||||
ModifyBoneTransform(targetTemplate, kvPair.Key, kvPair.Value);
|
||||
}
|
||||
|
||||
foreach (var boneName in deletedBones)
|
||||
{
|
||||
DeleteBoneTransform(targetTemplate, boneName);
|
||||
}
|
||||
|
||||
SaveTemplate(targetTemplate);
|
||||
}
|
||||
|
||||
//Creates, updates or deletes bone transform
|
||||
//not to be used on editor-related features by anything but TemplateEditorManager
|
||||
public bool ModifyBoneTransform(Template template, string boneName, BoneTransform transform)
|
||||
{
|
||||
if (template.Bones.TryGetValue(boneName, out var boneTransform)
|
||||
&& boneTransform != null)
|
||||
{
|
||||
if (boneTransform == transform)
|
||||
return false;
|
||||
|
||||
if (transform.IsEdited())
|
||||
{
|
||||
template.Bones[boneName].UpdateToMatch(transform);
|
||||
|
||||
_logger.Debug($"Updated bone {boneName} on {template.Name}");
|
||||
_event.Invoke(TemplateChanged.Type.UpdatedBone, template, boneName);
|
||||
}
|
||||
else
|
||||
{
|
||||
template.Bones.Remove(boneName);
|
||||
|
||||
_logger.Debug($"Deleted bone {boneName} on {template.Name}");
|
||||
_event.Invoke(TemplateChanged.Type.DeletedBone, template, boneName);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
template.Bones[boneName] = new BoneTransform(transform);
|
||||
|
||||
_logger.Debug($"Created bone {boneName} on {template.Name}");
|
||||
_event.Invoke(TemplateChanged.Type.NewBone, template, boneName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DeleteBoneTransform(Template template, string boneName)
|
||||
{
|
||||
if (!template.Bones.ContainsKey(boneName))
|
||||
return;
|
||||
|
||||
template.Bones.Remove(boneName);
|
||||
|
||||
_logger.Debug($"Deleted bone {boneName} on {template.Name}");
|
||||
_event.Invoke(TemplateChanged.Type.DeletedBone, template, boneName);
|
||||
}
|
||||
|
||||
private static void PruneIdempotentTransforms(Template template)
|
||||
{
|
||||
foreach (var kvp in template.Bones)
|
||||
{
|
||||
if (!kvp.Value.IsEdited())
|
||||
{
|
||||
template.Bones.Remove(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveTemplate(Template template)
|
||||
{
|
||||
template.ModifiedDate = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(template);
|
||||
}
|
||||
|
||||
private void OnReload(ReloadEvent.Type type)
|
||||
{
|
||||
if (type != ReloadEvent.Type.ReloadTemplates &&
|
||||
type != ReloadEvent.Type.ReloadAll)
|
||||
return;
|
||||
|
||||
_logger.Debug("Reload event received");
|
||||
LoadTemplates();
|
||||
}
|
||||
|
||||
private static void CreateTemplateFolder(SaveService service)
|
||||
{
|
||||
var ret = service.FileNames.TemplateDirectory;
|
||||
if (Directory.Exists(ret))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Error($"Could not create template 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<(Template, string)> invalidNames)
|
||||
{
|
||||
var failed = 0;
|
||||
foreach (var (template, name) in invalidNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var correctName = _saveService.FileNames.TemplateFile(template);
|
||||
File.Move(name, correctName, false);
|
||||
_logger.Information($"Moved invalid template file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
++failed;
|
||||
_logger.Error($"Failed to move invalid template file from {Path.GetFileName(name)}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
return failed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new guid until we find one which isn't used by existing template
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Guid CreateNewGuid()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
if (_templates.All(d => d.UniqueId != guid))
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user