mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
470 lines
19 KiB
C#
470 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Dalamud.Utility;
|
|
using Glamourer.Customization;
|
|
using Glamourer.Events;
|
|
using Glamourer.Services;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using OtterGui;
|
|
using Penumbra.GameData.Enums;
|
|
using Penumbra.GameData.Structs;
|
|
|
|
namespace Glamourer.Designs;
|
|
|
|
public class DesignManager
|
|
{
|
|
private readonly CustomizationService _customizations;
|
|
private readonly ItemManager _items;
|
|
private readonly SaveService _saveService;
|
|
private readonly DesignChanged _event;
|
|
private readonly List<Design> _designs = new();
|
|
|
|
public IReadOnlyList<Design> Designs
|
|
=> _designs;
|
|
|
|
public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations,
|
|
DesignChanged @event)
|
|
{
|
|
_saveService = saveService;
|
|
_items = items;
|
|
_customizations = customizations;
|
|
_event = @event;
|
|
CreateDesignFolder(saveService);
|
|
LoadDesigns();
|
|
MigrateOldDesigns();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear currently loaded designs and load all designs anew from file.
|
|
/// Invalid data is fixed, but changes are not saved until manual changes.
|
|
/// </summary>
|
|
public void LoadDesigns()
|
|
{
|
|
_designs.Clear();
|
|
List<(Design, string)> invalidNames = new();
|
|
var skipped = 0;
|
|
foreach (var file in _saveService.FileNames.Designs())
|
|
{
|
|
try
|
|
{
|
|
var text = File.ReadAllText(file.FullName);
|
|
var data = JObject.Parse(text);
|
|
var design = Design.LoadDesign(_customizations, _items, data);
|
|
if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(file.Name))
|
|
invalidNames.Add((design, file.FullName));
|
|
if (_designs.Any(f => f.Identifier == design.Identifier))
|
|
throw new Exception($"Identifier {design.Identifier} was not unique.");
|
|
|
|
design.Index = _designs.Count;
|
|
_designs.Add(design);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Glamourer.Log.Error($"Could not load design, skipped:\n{ex}");
|
|
++skipped;
|
|
}
|
|
}
|
|
|
|
var failed = MoveInvalidNames(invalidNames);
|
|
if (invalidNames.Count > 0)
|
|
Glamourer.Log.Information(
|
|
$"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}");
|
|
|
|
Glamourer.Log.Information(
|
|
$"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
|
|
_event.Invoke(DesignChanged.Type.ReloadedAll, null!);
|
|
}
|
|
|
|
/// <summary> Create a new design of a given name. </summary>
|
|
public Design Create(string name)
|
|
{
|
|
var design = new Design(_items)
|
|
{
|
|
CreationDate = DateTimeOffset.UtcNow,
|
|
LastEdit = DateTimeOffset.UtcNow,
|
|
Identifier = CreateNewGuid(),
|
|
Index = _designs.Count,
|
|
Name = name,
|
|
};
|
|
_designs.Add(design);
|
|
Glamourer.Log.Debug($"Added new design {design.Identifier}.");
|
|
_saveService.ImmediateSave(design);
|
|
_event.Invoke(DesignChanged.Type.Created, design);
|
|
return design;
|
|
}
|
|
|
|
/// <summary> Delete a design. </summary>
|
|
public void Delete(Design design)
|
|
{
|
|
foreach (var d in _designs.Skip(design.Index + 1))
|
|
--d.Index;
|
|
_designs.RemoveAt(design.Index);
|
|
_saveService.ImmediateDelete(design);
|
|
_event.Invoke(DesignChanged.Type.Deleted, design);
|
|
}
|
|
|
|
/// <summary> Rename a design. </summary>
|
|
public void Rename(Design design, string newName)
|
|
{
|
|
var oldName = design.Name.Text;
|
|
if (oldName == newName)
|
|
return;
|
|
|
|
design.Name = newName;
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug($"Renamed design {design.Identifier}.");
|
|
_event.Invoke(DesignChanged.Type.Renamed, design, oldName);
|
|
}
|
|
|
|
/// <summary> Change the description of a design. </summary>
|
|
public void ChangeDescription(Design design, string description)
|
|
{
|
|
var oldDescription = design.Description;
|
|
if (oldDescription == description)
|
|
return;
|
|
|
|
design.Description = description;
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug($"Changed description of design {design.Identifier}.");
|
|
_event.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription);
|
|
}
|
|
|
|
/// <summary> Add a new tag to a design. The tags remain sorted. </summary>
|
|
public void AddTag(Design design, string tag)
|
|
{
|
|
if (design.Tags.Contains(tag))
|
|
return;
|
|
|
|
design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray();
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
var idx = design.Tags.IndexOf(tag);
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}.");
|
|
_event.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx));
|
|
}
|
|
|
|
/// <summary> Remove a tag from a design if it exists. </summary>
|
|
public void RemoveTag(Design design, string tag)
|
|
=> RemoveTag(design, design.Tags.IndexOf(tag));
|
|
|
|
/// <summary> Remove a tag from a design by its index. </summary>
|
|
public void RemoveTag(Design design, int tagIdx)
|
|
{
|
|
if (tagIdx < 0 || tagIdx >= design.Tags.Length)
|
|
return;
|
|
|
|
var oldTag = design.Tags[tagIdx];
|
|
design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray();
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}.");
|
|
_event.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx));
|
|
}
|
|
|
|
/// <summary> Rename a tag from a design by its index. The tags stay sorted.</summary>
|
|
public void RenameTag(Design design, int tagIdx, string newTag)
|
|
{
|
|
var oldTag = design.Tags[tagIdx];
|
|
if (oldTag == newTag)
|
|
return;
|
|
|
|
design.Tags[tagIdx] = newTag;
|
|
Array.Sort(design.Tags);
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags.");
|
|
_event.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx));
|
|
}
|
|
|
|
/// <summary> Change a customization value. </summary>
|
|
public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value)
|
|
{
|
|
var oldValue = design.DesignData.Customize[idx];
|
|
switch (idx)
|
|
{
|
|
case CustomizeIndex.Race:
|
|
case CustomizeIndex.BodyType:
|
|
Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen.");
|
|
return;
|
|
case CustomizeIndex.Clan:
|
|
if (!_customizations.ChangeClan(ref design.DesignData.Customize, (SubRace)value.Value))
|
|
return;
|
|
|
|
break;
|
|
case CustomizeIndex.Gender:
|
|
if (!_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1)))
|
|
return;
|
|
|
|
break;
|
|
default:
|
|
if (!design.DesignData.Customize.Set(idx, value))
|
|
return;
|
|
|
|
break;
|
|
}
|
|
|
|
Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}.");
|
|
_saveService.QueueSave(design);
|
|
_event.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx));
|
|
}
|
|
|
|
/// <summary> Change whether to apply a specific customize value. </summary>
|
|
public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
|
|
{
|
|
if (!design.SetApplyCustomize(idx, value))
|
|
return;
|
|
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}.");
|
|
_event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx);
|
|
}
|
|
|
|
/// <summary> Change a non-weapon equipment piece. </summary>
|
|
public void ChangeEquip(Design design, EquipSlot slot, EquipItem item)
|
|
{
|
|
if (_items.ValidateItem(slot, item.Id, out item).Length > 0)
|
|
return;
|
|
|
|
var old = design.DesignData.Item(slot);
|
|
if (!design.DesignData.SetItem(slot, item))
|
|
return;
|
|
|
|
Glamourer.Log.Debug(
|
|
$"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}).");
|
|
_saveService.QueueSave(design);
|
|
_event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot));
|
|
}
|
|
|
|
/// <summary> Change a weapon. </summary>
|
|
public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item)
|
|
{
|
|
var currentMain = design.DesignData.Item(EquipSlot.MainHand);
|
|
var currentOff = design.DesignData.Item(EquipSlot.OffHand);
|
|
switch (slot)
|
|
{
|
|
case EquipSlot.MainHand:
|
|
var newOff = currentOff;
|
|
if (item.Type == currentMain.Type)
|
|
{
|
|
if (_items.ValidateWeapons(item.Id, currentOff.Id, out _, out _).Length != 0)
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
var newOffId = FullEquipTypeExtensions.OffhandTypes.Contains(item.Type)
|
|
? item.Id
|
|
: ItemManager.NothingId(item.Type.Offhand());
|
|
if (_items.ValidateWeapons(item.Id, newOffId, out _, out newOff).Length != 0)
|
|
return;
|
|
}
|
|
|
|
design.DesignData.SetItem(EquipSlot.MainHand, item);
|
|
design.DesignData.SetItem(EquipSlot.OffHand, newOff);
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug(
|
|
$"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.Id}) to {item.Name} ({item.Id}).");
|
|
_event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff));
|
|
return;
|
|
case EquipSlot.OffHand:
|
|
if (item.Type != currentOff.Type)
|
|
return;
|
|
if (_items.ValidateWeapons(currentMain.Id, item.Id, out _, out _).Length > 0)
|
|
return;
|
|
|
|
if (!design.DesignData.SetItem(EquipSlot.OffHand, item))
|
|
return;
|
|
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug(
|
|
$"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.Id}) to {item.Name} ({item.Id}).");
|
|
_event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item));
|
|
return;
|
|
default: return;
|
|
}
|
|
}
|
|
|
|
/// <summary> Change whether to apply a specific equipment piece. </summary>
|
|
public void ChangeApplyEquip(Design design, EquipSlot slot, bool value)
|
|
{
|
|
if (!design.SetApplyEquip(slot, value))
|
|
return;
|
|
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}.");
|
|
_event.Invoke(DesignChanged.Type.ApplyEquip, design, slot);
|
|
}
|
|
|
|
/// <summary> Change the stain for any equipment piece. </summary>
|
|
public void ChangeStain(Design design, EquipSlot slot, StainId stain)
|
|
{
|
|
if (_items.ValidateStain(stain, out _).Length > 0)
|
|
return;
|
|
|
|
var oldStain = design.DesignData.Stain(slot);
|
|
if (!design.DesignData.SetStain(slot, stain))
|
|
return;
|
|
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}.");
|
|
_event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot));
|
|
}
|
|
|
|
/// <summary> Change whether to apply a specific stain. </summary>
|
|
public void ChangeApplyStain(Design design, EquipSlot slot, bool value)
|
|
{
|
|
if (!design.SetApplyStain(slot, value))
|
|
return;
|
|
|
|
design.LastEdit = DateTimeOffset.UtcNow;
|
|
_saveService.QueueSave(design);
|
|
Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}.");
|
|
_event.Invoke(DesignChanged.Type.ApplyStain, design, slot);
|
|
}
|
|
|
|
private void MigrateOldDesigns()
|
|
{
|
|
if (!File.Exists(_saveService.FileNames.MigrationDesignFile))
|
|
return;
|
|
|
|
var errors = 0;
|
|
var skips = 0;
|
|
var successes = 0;
|
|
try
|
|
{
|
|
var text = File.ReadAllText(_saveService.FileNames.MigrationDesignFile);
|
|
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(text) ?? new Dictionary<string, string>();
|
|
var migratedFileSystemPaths = new Dictionary<string, string>(dict.Count);
|
|
foreach (var (name, base64) in dict)
|
|
{
|
|
try
|
|
{
|
|
var actualName = Path.GetFileName(name);
|
|
var design = new Design(_items)
|
|
{
|
|
CreationDate = File.GetCreationTimeUtc(_saveService.FileNames.MigrationDesignFile),
|
|
LastEdit = File.GetLastWriteTimeUtc(_saveService.FileNames.MigrationDesignFile),
|
|
Identifier = CreateNewGuid(),
|
|
Name = actualName,
|
|
};
|
|
design.MigrateBase64(_items, base64);
|
|
if (!_designs.Any(d => d.Name == design.Name && d.CreationDate == design.CreationDate))
|
|
{
|
|
Add(design, $"Migrated old design to {design.Identifier}.");
|
|
migratedFileSystemPaths.Add(design.Identifier.ToString(), name);
|
|
++successes;
|
|
}
|
|
else
|
|
{
|
|
Glamourer.Log.Debug(
|
|
"Skipped migrating old design because a design of the same name and creation date already existed.");
|
|
++skips;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Glamourer.Log.Error($"Could not migrate design {name}:\n{ex}");
|
|
++errors;
|
|
}
|
|
}
|
|
|
|
DesignFileSystem.MigrateOldPaths(_saveService, migratedFileSystemPaths);
|
|
Glamourer.Log.Information(
|
|
$"Successfully migrated {successes} old designs. Skipped {skips} already migrated designs. Failed to migrate {errors} designs.");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Glamourer.Log.Error($"Could not migrate old design file {_saveService.FileNames.MigrationDesignFile}:\n{e}");
|
|
}
|
|
|
|
try
|
|
{
|
|
File.Move(_saveService.FileNames.MigrationDesignFile,
|
|
Path.ChangeExtension(_saveService.FileNames.MigrationDesignFile, ".json.bak"));
|
|
Glamourer.Log.Information($"Moved migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Glamourer.Log.Error($"Could not move migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file:\n{ex}");
|
|
}
|
|
}
|
|
|
|
/// <summary> Try to ensure existence of the design folder. </summary>
|
|
private static void CreateDesignFolder(SaveService service)
|
|
{
|
|
var ret = service.FileNames.DesignDirectory;
|
|
if (Directory.Exists(ret))
|
|
return;
|
|
|
|
try
|
|
{
|
|
Directory.CreateDirectory(ret);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Glamourer.Log.Error($"Could not create design folder directory at {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<(Design, string)> invalidNames)
|
|
{
|
|
var failed = 0;
|
|
foreach (var (design, name) in invalidNames)
|
|
{
|
|
try
|
|
{
|
|
var correctName = _saveService.FileNames.DesignFile(design);
|
|
File.Move(name, correctName, false);
|
|
Glamourer.Log.Information($"Moved invalid design file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
++failed;
|
|
Glamourer.Log.Error($"Failed to move invalid design file from {Path.GetFileName(name)}:\n{ex}");
|
|
}
|
|
}
|
|
|
|
return failed;
|
|
}
|
|
|
|
/// <summary> Create new GUIDs until we have one that is not in use. </summary>
|
|
private Guid CreateNewGuid()
|
|
{
|
|
while (true)
|
|
{
|
|
var guid = Guid.NewGuid();
|
|
if (_designs.All(d => d.Identifier != guid))
|
|
return guid;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to add an external design to the list.
|
|
/// Returns false if the design is already contained or if the identifier is already in use.
|
|
/// The design is treated as newly created and invokes an event.
|
|
/// </summary>
|
|
private bool Add(Design design, string? message)
|
|
{
|
|
if (_designs.Any(d => d == design || d.Identifier == design.Identifier))
|
|
return false;
|
|
|
|
design.Index = _designs.Count;
|
|
_designs.Add(design);
|
|
if (!message.IsNullOrEmpty())
|
|
Glamourer.Log.Debug(message);
|
|
_saveService.ImmediateSave(design);
|
|
_event.Invoke(DesignChanged.Type.Created, design);
|
|
return true;
|
|
}
|
|
}
|