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 _designs = new(); public IReadOnlyList Designs => _designs; public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations, DesignChanged @event) { _saveService = saveService; _items = items; _customizations = customizations; _event = @event; CreateDesignFolder(saveService); LoadDesigns(); MigrateOldDesigns(); } /// /// Clear currently loaded designs and load all designs anew from file. /// Invalid data is fixed, but changes are not saved until manual changes. /// 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!); } /// Create a new design of a given name. 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; } /// Delete a design. 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); } /// Rename a design. 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); } /// Change the description of a design. 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); } /// Add a new tag to a design. The tags remain sorted. 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)); } /// Remove a tag from a design if it exists. public void RemoveTag(Design design, string tag) => RemoveTag(design, design.Tags.IndexOf(tag)); /// Remove a tag from a design by its index. 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)); } /// Rename a tag from a design by its index. The tags stay sorted. 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)); } /// Change a customization value. 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)); } /// Change whether to apply a specific customize value. 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); } /// Change a non-weapon equipment piece. 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)); } /// Change a weapon. 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; } } /// Change whether to apply a specific equipment piece. 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); } /// Change the stain for any equipment piece. 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)); } /// Change whether to apply a specific stain. 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>(text) ?? new Dictionary(); var migratedFileSystemPaths = new Dictionary(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}"); } } /// Try to ensure existence of the design folder. 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}"); } } /// Move all files that were discovered to have names not corresponding to their identifier to correct names, if possible. /// The number of files that could not be moved. 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; } /// Create new GUIDs until we have one that is not in use. private Guid CreateNewGuid() { while (true) { var guid = Guid.NewGuid(); if (_designs.All(d => d.Identifier != guid)) return guid; } } /// /// 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. /// 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; } }