diff --git a/Glamourer.GameData/Structs/Item.cs b/Glamourer.GameData/Structs/Item.cs index e952c7f..f4c519e 100644 --- a/Glamourer.GameData/Structs/Item.cs +++ b/Glamourer.GameData/Structs/Item.cs @@ -24,8 +24,8 @@ public readonly struct Item public bool IsBothHand => (EquipSlot)Base.EquipSlotCategory.Row == EquipSlot.BothHand; - public WeaponCategory WeaponCategory - => (WeaponCategory?) Base.ItemUICategory?.Row ?? WeaponCategory.Unknown; + public FullEquipType WeaponCategory + => ((WeaponCategory) (Base.ItemUICategory?.Row ?? 0)).ToEquipType(); // Create a new item from its sheet list with the given name and either the inferred equip slot or the given one. public Item(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown) diff --git a/Glamourer.zip b/Glamourer.zip index 59fe7e1..ba2992d 100644 Binary files a/Glamourer.zip and b/Glamourer.zip differ diff --git a/Glamourer/Designs/Design.Manager.cs b/Glamourer/Designs/Design.Manager.cs new file mode 100644 index 0000000..6e0bdd6 --- /dev/null +++ b/Glamourer/Designs/Design.Manager.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Dalamud.Plugin; +using ImGuizmoNET; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Filesystem; + +namespace Glamourer.Designs; + +public sealed class DesignFileSystem : FileSystem, IDisposable +{ + public readonly string DesignFileSystemFile; + private readonly Design.Manager _designManager; + + public DesignFileSystem(Design.Manager designManager, DalamudPluginInterface pi) + { + DesignFileSystemFile = Path.Combine(pi.GetPluginConfigDirectory(), "sort_order.json"); + _designManager = designManager; + } + + public struct CreationDate : ISortMode + { + public string Name + => "Creation Date (Older First)"; + + public string Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."; + + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate)); + } + + public struct InverseCreationDate : ISortMode + { + public string Name + => "Creation Date (Newer First)"; + + public string Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."; + + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate)); + } + + private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3) + { + if (type != FileSystemChangeType.Reload) + { + SaveFilesystem(); + } + } + + private void SaveFilesystem() + { + SaveToFile(new FileInfo(DesignFileSystemFile), SaveDesign, true); + Glamourer.Log.Verbose($"Saved design filesystem."); + } + + private void Save() + => Glamourer.Framework.RegisterDelayed(nameof(SaveFilesystem), SaveFilesystem); + + private void OnDataChange(Design.Manager.DesignChangeType type, Design design, string? oldName, string? _2, int _3) + { + switch (type) + { + + } + if (type == Design.Manager.DesignChangeType.Renamed && oldName != null) + { + var old = oldName.FixName(); + if (Find(old, out var child) && child is not Folder) + { + Rename(child, design.Name); + } + } + + + } + + // Used for saving and loading. + private static string DesignToIdentifier(Design design) + => design.Identifier.ToString(); + + private static string DesignToName(Design design) + => design.Name.FixName(); + + private static bool DesignHasDefaultPath(Design design, string fullPath) + { + var regex = new Regex($@"^{Regex.Escape(DesignToName(design))}( \(\d+\))?$"); + return regex.IsMatch(fullPath); + } + + private static (string, bool) SaveDesign(Design design, string fullPath) + // Only save pairs with non-default paths. + => DesignHasDefaultPath(design, fullPath) + ? (string.Empty, false) + : (DesignToName(design), true); +} + +public partial class Design +{ + public partial class Manager + { + public const string DesignFolderName = "designs"; + public readonly string DesignFolder; + + private readonly List _designs = new(); + + public enum DesignChangeType + { + Created, + Deleted, + ReloadedAll, + Renamed, + ChangedDescription, + AddedTag, + RemovedTag, + ChangedTag, + } + + public delegate void DesignChangeDelegate(DesignChangeType type, int designIdx, string? oldData = null, string? newData = null, + int tagIdx = -1); + + public event DesignChangeDelegate? DesignChange; + + public IReadOnlyList Designs + => _designs; + + public Manager(DalamudPluginInterface pi) + => DesignFolder = SetDesignFolder(pi); + + private static string SetDesignFolder(DalamudPluginInterface pi) + { + var ret = Path.Combine(pi.GetPluginConfigDirectory(), DesignFolderName); + if (Directory.Exists(ret)) + return ret; + + try + { + Directory.CreateDirectory(ret); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not create design folder directory at {ret}:\n{ex}"); + } + + return ret; + } + + private string CreateFileName(Design design) + => Path.Combine(DesignFolder, $"{design.Name.RemoveInvalidPathSymbols()}_{design.Identifier}.json"); + + public void SaveDesign(Design design) + { + var fileName = CreateFileName(design); + try + { + var data = design.JsonSerialize().ToString(Formatting.Indented); + File.WriteAllText(fileName, data); + Glamourer.Log.Debug($"Saved design {design.Identifier}."); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not save design {design.Identifier} to file:\n{ex}"); + } + } + + public void LoadDesigns() + { + _designs.Clear(); + foreach (var file in new DirectoryInfo(DesignFolder).EnumerateFiles("*.json", SearchOption.TopDirectoryOnly)) + { + try + { + var text = File.ReadAllText(file.FullName); + var data = JObject.Parse(text); + var design = LoadDesign(data); + design.Index = _designs.Count; + _designs.Add(design); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not load design, skipped:\n{ex}"); + } + } + + Glamourer.Log.Information($"Loaded {_designs.Count} designs."); + DesignChange?.Invoke(DesignChangeType.ReloadedAll, -1); + } + + public Design Create(string name) + { + var design = new Design() + { + CreationDate = DateTimeOffset.UtcNow, + Identifier = Guid.NewGuid(), + Index = _designs.Count, + Name = name, + }; + _designs.Add(design); + Glamourer.Log.Debug($"Added new design {design.Identifier}."); + DesignChange?.Invoke(DesignChangeType.Created, design.Index); + return design; + } + + public void Delete(Design design) + { + _designs.RemoveAt(design.Index); + foreach (var d in _designs.Skip(design.Index + 1)) + --d.Index; + var fileName = CreateFileName(design); + try + { + File.Delete(fileName); + Glamourer.Log.Debug($"Deleted design {design.Identifier}."); + DesignChange?.Invoke(DesignChangeType.Deleted, design.Index); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not delete design file for {design.Identifier}:\n{ex}"); + } + } + + public void Rename(Design design, string newName) + { + var oldName = design.Name; + var oldFileName = CreateFileName(design); + if (File.Exists(oldFileName)) + try + { + File.Delete(oldFileName); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not delete old design file for rename from {design.Identifier}:\n{ex}"); + return; + } + + design.Name = newName; + SaveDesign(design); + Glamourer.Log.Debug($"Renamed design {design.Identifier}."); + DesignChange?.Invoke(DesignChangeType.Renamed, design.Index, oldName, newName); + } + + public void ChangeDescription(Design design, string description) + { + var oldDescription = design.Description; + design.Description = description; + SaveDesign(design); + Glamourer.Log.Debug($"Renamed design {design.Identifier}."); + DesignChange?.Invoke(DesignChangeType.ChangedDescription, design.Index, oldDescription, description); + } + + public void AddTag(Design design, string tag) + { + if (design.Tags.Contains(tag)) + return; + + design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray(); + var idx = design.Tags.IndexOf(tag); + SaveDesign(design); + Glamourer.Log.Debug($"Added tag at {idx} to design {design.Identifier}."); + DesignChange?.Invoke(DesignChangeType.AddedTag, design.Index, null, tag, idx); + } + + public void RemoveTag(Design design, string tag) + { + var idx = design.Tags.IndexOf(tag); + if (idx >= 0) + RemoveTag(design, idx); + } + + public void RemoveTag(Design design, int tagIdx) + { + var oldTag = design.Tags[tagIdx]; + design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray(); + SaveDesign(design); + Glamourer.Log.Debug($"Removed tag at {tagIdx} from design {design.Identifier}."); + DesignChange?.Invoke(DesignChangeType.RemovedTag, design.Index, oldTag, null, tagIdx); + } + + + 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); + SaveDesign(design); + Glamourer.Log.Debug($"Renamed tag at {tagIdx} in design {design.Identifier} and resorted tags."); + DesignChange?.Invoke(DesignChangeType.ChangedTag, design.Index, oldTag, newTag, tagIdx); + } + } +} diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs new file mode 100644 index 0000000..d7a5e96 --- /dev/null +++ b/Glamourer/Designs/Design.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace Glamourer.Designs; + +public partial class Design +{ + public Guid Identifier { get; private init; } + public DateTimeOffset CreationDate { get; private init; } + public string Name { get; private set; } = string.Empty; + public string Description { get; private set; } = string.Empty; + public string[] Tags { get; private set; } = Array.Empty(); + public int Index { get; private set; } + + public JObject JsonSerialize() + { + var ret = new JObject + { + [nameof(Identifier)] = Identifier, + [nameof(CreationDate)] = CreationDate, + [nameof(Name)] = Name, + [nameof(Description)] = Description, + [nameof(Tags)] = JArray.FromObject(Tags), + }; + return ret; + } + + public static Design LoadDesign(JObject json) + => new() + { + CreationDate = json[nameof(CreationDate)]?.ToObject() ?? throw new ArgumentNullException(nameof(CreationDate)), + Identifier = json[nameof(Identifier)]?.ToObject() ?? throw new ArgumentNullException(nameof(Identifier)), + Name = json[nameof(Name)]?.ToObject() ?? throw new ArgumentNullException(nameof(Name)), + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Tags = ParseTags(json), + }; + + private static string[] ParseTags(JObject json) + { + var tags = json[nameof(Tags)]?.ToObject() ?? Array.Empty(); + return tags.OrderBy(t => t).Distinct().ToArray(); + } +} diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs deleted file mode 100644 index 2d6f434..0000000 --- a/Glamourer/Designs/DesignManager.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Newtonsoft.Json; - -namespace Glamourer.Designs; - -public class DesignManager -{ - //public const string FileName = "Designs.json"; - //private readonly FileInfo _saveFile; - // - //public SortedList Designs = null!; - //public FileSystem.FileSystem FileSystem { get; } = new(); - // - //public DesignManager() - //{ - // var saveFolder = new DirectoryInfo(Dalamud.PluginInterface.GetPluginConfigDirectory()); - // if (!saveFolder.Exists) - // Directory.CreateDirectory(saveFolder.FullName); - // - // _saveFile = new FileInfo(Path.Combine(saveFolder.FullName, FileName)); - // - // LoadFromFile(); - //} - // - //private void BuildStructure() - //{ - // FileSystem.Clear(); - // var anyChanges = false; - // foreach (var (path, save) in Designs.ToArray()) - // { - // try - // { - // var (folder, name) = FileSystem.CreateAllFolders(path); - // var design = new Design(folder, name) { Data = save }; - // folder.FindOrAddChild(design); - // var fixedPath = design.FullName(); - // if (string.Equals(fixedPath, path, StringComparison.InvariantCultureIgnoreCase)) - // continue; - // - // Designs.Remove(path); - // Designs[fixedPath] = save; - // anyChanges = true; - // PluginLog.Debug($"Problem loading saved designs, {path} was renamed to {fixedPath}."); - // } - // catch (Exception e) - // { - // PluginLog.Error($"Problem loading saved designs, {path} was removed because:\n{e}"); - // Designs.Remove(path); - // } - // } - // - // if (anyChanges) - // SaveToFile(); - //} - // - //private bool UpdateRoot(string oldPath, Design child) - //{ - // var newPath = child.FullName(); - // if (string.Equals(newPath, oldPath, StringComparison.InvariantCultureIgnoreCase)) - // return false; - // - // Designs.Remove(oldPath); - // Designs[child.FullName()] = child.Data; - // return true; - //} - // - //private void UpdateChild(string oldRootPath, string newRootPath, Design child) - //{ - // var newPath = child.FullName(); - // var oldPath = $"{oldRootPath}{newPath.Remove(0, newRootPath.Length)}"; - // Designs.Remove(oldPath); - // Designs[newPath] = child.Data; - //} - // - //public void DeleteAllChildren(IFileSystemBase root, bool deleteEmpty) - //{ - // if (root is Folder f) - // foreach (var child in f.AllLeaves(SortMode.Lexicographical)) - // Designs.Remove(child.FullName()); - // var fullPath = root.FullName(); - // root.Parent.RemoveChild(root, deleteEmpty); - // Designs.Remove(fullPath); - // - // SaveToFile(); - //} - // - //public void UpdateAllChildren(string oldPath, IFileSystemBase root) - //{ - // var changes = false; - // switch (root) - // { - // case Design d: - // changes |= UpdateRoot(oldPath, d); - // break; - // case Folder f: - // { - // var newRootPath = root.FullName(); - // if (!string.Equals(oldPath, newRootPath, StringComparison.InvariantCultureIgnoreCase)) - // { - // changes = true; - // foreach (var descendant in f.AllLeaves(SortMode.Lexicographical).Where(l => l is Design).Cast()) - // UpdateChild(oldPath, newRootPath, descendant); - // } - // - // break; - // } - // } - // - // if (changes) - // SaveToFile(); - //} - // - //public void SaveToFile() - //{ - // try - // { - // var data = JsonConvert.SerializeObject(Designs, Formatting.Indented); - // File.WriteAllText(_saveFile.FullName, data); - // } - // catch (Exception e) - // { - // PluginLog.Error($"Could not write to save file {_saveFile.FullName}:\n{e}"); - // } - //} - // - //public void LoadFromFile() - //{ - // _saveFile.Refresh(); - // Designs = new SortedList(); - // var changes = false; - // if (_saveFile.Exists) - // try - // { - // var data = File.ReadAllText(_saveFile.FullName); - // var json = JsonConvert.DeserializeObject>(data); - // if (json == null) - // { - // PluginLog.Error($"Save file {_saveFile.FullName} corrupted."); - // json = new Dictionary(); - // } - - // foreach (var (name, saveString) in json) - // { - // try - // { - // var save = CharacterSave.FromString(saveString, out var oldVersion); - // changes |= oldVersion; - // changes |= !Designs.TryAdd(name, save); - // } - // catch (Exception e) - // { - // PluginLog.Error($"Character Save for {name} is invalid:\n{e}"); - // changes = true; - // } - // } - // } - // catch (Exception e) - // { - // PluginLog.Error($"Could not load save file {_saveFile.FullName}:\n{e}"); - // changes = true; - // } - // else - // changes = true; - - // if (changes) - // SaveToFile(); - - // BuildStructure(); - //} -} diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index b60b9f7..f415c72 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -96,7 +96,6 @@ public class Glamourer : IDalamudPlugin public void Dispose() { - Dalamud.PluginInterface.RelinquishData("test1"); RedrawManager?.Dispose(); Penumbra?.Dispose(); if (_windowSystem != null) diff --git a/Glamourer/Gui/InterfaceDesigns.cs b/Glamourer/Gui/Designs/InterfaceDesigns.cs similarity index 95% rename from Glamourer/Gui/InterfaceDesigns.cs rename to Glamourer/Gui/Designs/InterfaceDesigns.cs index 6e083b8..8621ff7 100644 --- a/Glamourer/Gui/InterfaceDesigns.cs +++ b/Glamourer/Gui/Designs/InterfaceDesigns.cs @@ -1,15 +1,6 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Logging; -using Glamourer.Designs; -using Glamourer.Structs; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; +namespace Glamourer.Gui.Designs; + -namespace Glamourer.Gui; //internal partial class Interface //{ diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.Items.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.Items.cs index 5e9bcf9..f65fa16 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.Items.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.Items.cs @@ -91,7 +91,7 @@ public partial class EquipmentDrawer private CharacterWeapon _lastWeapon = new(ulong.MaxValue); private string _lastPreview = string.Empty; private int _lastIndex; - public WeaponCategory LastCategory { get; private set; } + public FullEquipType LastCategory { get; private set; } private bool _drawAll; public WeaponCombo(EquipSlot slot) @@ -124,12 +124,12 @@ public partial class EquipmentDrawer } UpdateItem(weapon); - UpdateCategory((WeaponCategory?)LastItem!.ItemUICategory?.Row ?? WeaponCategory.Unknown); + UpdateCategory(((WeaponCategory) (LastItem!.ItemUICategory?.Row ?? 0)).ToEquipType()); newIdx = _lastIndex; return Draw(Label, _lastPreview, ref newIdx, ItemComboWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeight()); } - public bool Draw(CharacterWeapon weapon, WeaponCategory category, out int newIdx) + public bool Draw(CharacterWeapon weapon, FullEquipType category, out int newIdx) { if (_drawAll) { @@ -166,7 +166,7 @@ public partial class EquipmentDrawer _lastPreview = _lastIndex >= 0 ? Items[_lastIndex].Name : LastItem.Name.ToString(); } - private void UpdateCategory(WeaponCategory category) + private void UpdateCategory(FullEquipType category) { if (category == LastCategory) return; @@ -225,7 +225,7 @@ public partial class EquipmentDrawer Glamourer.RedrawManager.LoadWeapon(actor, _currentSlot, mainHand); } - private void DrawOffHandSelector(ref CharacterWeapon offHand, WeaponCategory category) + private void DrawOffHandSelector(ref CharacterWeapon offHand, FullEquipType category) { var change = OffHandCombo.Draw(offHand, category, out var newIdx); var newWeapon = change ? ToWeapon(OffHandCombo.Items[newIdx], offHand.Stain) : CharacterWeapon.Empty; diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.Main.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.Main.cs index f434adf..e2c824d 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.Main.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.Main.cs @@ -100,8 +100,8 @@ public partial class EquipmentDrawer DrawStainCombo(); ImGui.SameLine(); DrawMainHandSelector(ref mainHand); - var offhand = MainHandCombo.LastCategory.AllowsOffHand(); - if (offhand != WeaponCategory.Unknown) + var offhand = MainHandCombo.LastCategory.Offhand(); + if (offhand != FullEquipType.Unknown) { _currentSlot = EquipSlot.OffHand; DrawStainCombo(); @@ -128,8 +128,8 @@ public partial class EquipmentDrawer DrawStainCombo(); ImGui.SameLine(); DrawMainHandSelector(ref mainHand); - var offhand = MainHandCombo.LastCategory.AllowsOffHand(); - if (offhand != WeaponCategory.Unknown) + var offhand = MainHandCombo.LastCategory.Offhand(); + if (offhand != FullEquipType.Unknown) { _currentSlot = EquipSlot.OffHand; DrawCheckbox(ref flags); diff --git a/Glamourer/Interop/Actor.cs b/Glamourer/Interop/Actor.cs index 204e425..bf781b1 100644 --- a/Glamourer/Interop/Actor.cs +++ b/Glamourer/Interop/Actor.cs @@ -26,7 +26,7 @@ public unsafe partial struct Actor : IEquatable, IDesignable => actor.Pointer == null ? IntPtr.Zero : (IntPtr)actor.Pointer; public ActorIdentifier GetIdentifier() - => Glamourer.Actors.FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Pointer); + => Glamourer.Actors.FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Pointer, out _, true, true); public bool Identifier(out ActorIdentifier ident) { diff --git a/Glamourer/Interop/RedrawManager.Weapons.cs b/Glamourer/Interop/RedrawManager.Weapons.cs index 6b949ed..00809bf 100644 --- a/Glamourer/Interop/RedrawManager.Weapons.cs +++ b/Glamourer/Interop/RedrawManager.Weapons.cs @@ -24,7 +24,7 @@ public unsafe partial class RedrawManager // redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one. // skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change) // unk4 seemed to be the same as unk1. - [Signature("E8 ?? ?? ?? ?? 44 8B 9F", DetourName = nameof(LoadWeaponDetour))] + [Signature(Penumbra.GameData.Sigs.WeaponReload, DetourName = nameof(LoadWeaponDetour))] private readonly Hook _loadWeaponHook = null!; private void LoadWeaponDetour(IntPtr characterOffset, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, diff --git a/Glamourer/Interop/RedrawManager.cs b/Glamourer/Interop/RedrawManager.cs index 5739377..6815d08 100644 --- a/Glamourer/Interop/RedrawManager.cs +++ b/Glamourer/Interop/RedrawManager.cs @@ -102,9 +102,5 @@ public unsafe partial class RedrawManager : IDisposable private static void OnCharacterRedrawFinished(IntPtr gameObject, string collection, IntPtr drawObject) { //SetVisor((Human*)drawObject, true); - if (Glamourer.Models.FromCharacterBase((CharacterBase*)drawObject, out var data)) - PluginLog.Information($"Name: {data.FirstName} ({data.Id})"); - else - PluginLog.Information($"Key: {Glamourer.Models.KeyFromCharacterBase((CharacterBase*)drawObject):X16}"); } } \ No newline at end of file