diff --git a/Glamourer.GameData/Customization/CustomizeFlag.cs b/Glamourer.GameData/Customization/CustomizeFlag.cs index 867d095..2529678 100644 --- a/Glamourer.GameData/Customization/CustomizeFlag.cs +++ b/Glamourer.GameData/Customization/CustomizeFlag.cs @@ -47,6 +47,8 @@ public enum CustomizeFlag : ulong public static class CustomizeFlagExtensions { + public const CustomizeFlag All = (CustomizeFlag)(((ulong)CustomizeFlag.FacePaintColor << 1) - 1ul); + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static CustomizeIndex ToIndex(this CustomizeFlag flag) => flag switch @@ -87,6 +89,6 @@ public static class CustomizeFlagExtensions CustomizeFlag.FacePaint => CustomizeIndex.FacePaint, CustomizeFlag.FacePaintReversed => CustomizeIndex.FacePaintReversed, CustomizeFlag.FacePaintColor => CustomizeIndex.FacePaintColor, - _ => (CustomizeIndex) byte.MaxValue, + _ => (CustomizeIndex)byte.MaxValue, }; } diff --git a/Glamourer.GameData/GameData.cs b/Glamourer.GameData/GameData.cs index 61babb1..721cc09 100644 --- a/Glamourer.GameData/GameData.cs +++ b/Glamourer.GameData/GameData.cs @@ -6,30 +6,29 @@ using Dalamud.Data; using Glamourer.Structs; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; -using Item = Glamourer.Structs.Item; namespace Glamourer; public static class GameData { - private static Dictionary>? _itemsBySlot; + private static Dictionary>? _itemsBySlot; private static Dictionary? _jobs; private static Dictionary? _jobGroups; - public static IReadOnlyDictionary> ItemsBySlot(DataManager dataManager) + public static IReadOnlyDictionary> ItemsBySlot(DataManager dataManager) { if (_itemsBySlot != null) return _itemsBySlot; var sheet = dataManager.GetExcelSheet()!; - Item EmptySlot(EquipSlot slot) + Item2 EmptySlot(EquipSlot slot) => new(sheet.First(), "Nothing", slot); - static Item EmptyNpc(EquipSlot slot) + static Item2 EmptyNpc(EquipSlot slot) => new(new Lumina.Excel.GeneratedSheets.Item() { ModelMain = 9903 }, "Smallclothes (NPC)", slot); - _itemsBySlot = new Dictionary>() + _itemsBySlot = new Dictionary>() { [EquipSlot.Head] = new(200) { @@ -93,7 +92,7 @@ public static class GameData if (slot == EquipSlot.OffHand) slot = EquipSlot.MainHand; if (_itemsBySlot.TryGetValue(slot, out var list)) - list.Add(new Item(item, name, slot)); + list.Add(new Item2(item, name, slot)); } foreach (var list in _itemsBySlot.Values) diff --git a/Glamourer.GameData/Offsets.cs b/Glamourer.GameData/Offsets.cs new file mode 100644 index 0000000..ce3f6ae --- /dev/null +++ b/Glamourer.GameData/Offsets.cs @@ -0,0 +1,34 @@ +namespace Glamourer; + +public static class Offsets +{ + public static class Character + { + public const int ClassJobContainer = 0x1A8; + + public const int Wetness = 0x1ADA; + public const int HatVisible = 0x84E; + public const int VisorToggled = 0x84F; + public const int WeaponHidden1 = 0x84F; + public const int WeaponHidden2 = 0x72C; + public const int Alpha = 0x19E0; + + public static class Flags + { + public const byte IsHatHidden = 0x01; + public const byte IsVisorToggled = 0x08; + public const byte IsWet = 0x80; + public const byte IsWeaponHidden1 = 0x01; + public const byte IsWeaponHidden2 = 0x02; + } + } + + public const byte DrawObjectVisorStateFlag = 0x40; + public const byte DrawObjectVisorToggleFlag = 0x80; +} + +public static class Sigs +{ + public const string ChangeJob = "88 51 ?? 44 3B CA"; + public const string FlagSlotForUpdate = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A"; +} diff --git a/Glamourer.GameData/Structs/Item.cs b/Glamourer.GameData/Structs/Item2.cs similarity index 88% rename from Glamourer.GameData/Structs/Item.cs rename to Glamourer.GameData/Structs/Item2.cs index f4c519e..ad87191 100644 --- a/Glamourer.GameData/Structs/Item.cs +++ b/Glamourer.GameData/Structs/Item2.cs @@ -4,7 +4,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Structs; // An Item wrapper struct that contains the item table, a precomputed name and the associated equip slot. -public readonly struct Item +public readonly struct Item2 { public readonly Lumina.Excel.GeneratedSheets.Item Base; public readonly string Name; @@ -28,7 +28,7 @@ public readonly struct Item => ((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) + public Item2(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown) { Base = item; Name = name; @@ -36,7 +36,7 @@ public readonly struct Item } // Create empty Nothing items. - public static Item Nothing(EquipSlot slot) + public static Item2 Nothing(EquipSlot slot) => new("Nothing", slot); // Produce the relevant model information for a given item and equip slot. @@ -58,7 +58,7 @@ public readonly struct Item } // Used for 'Nothing' items. - private Item(string name, EquipSlot slot) + private Item2(string name, EquipSlot slot) { Name = name; Base = new Lumina.Excel.GeneratedSheets.Item(); diff --git a/Glamourer/Api/PenumbraAttach.cs b/Glamourer/Api/PenumbraAttach.cs index 241cbad..4b97e2a 100644 --- a/Glamourer/Api/PenumbraAttach.cs +++ b/Glamourer/Api/PenumbraAttach.cs @@ -109,42 +109,42 @@ public unsafe class PenumbraAttach : IDisposable return; var item = (Lumina.Excel.GeneratedSheets.Item)type.GetObject(Dalamud.GameData, id)!; - var writeItem = new Item(item, string.Empty); + var writeItem = new Item2(item, string.Empty); UpdateItem(ObjectManager.GPosePlayer, writeItem); UpdateItem(ObjectManager.Player, writeItem); } - private static void UpdateItem(Actor actor, Item item) + private static void UpdateItem(Actor actor, Item2 item2) { if (!actor || !actor.DrawObject) return; - switch (item.EquippableTo) + switch (item2.EquippableTo) { case EquipSlot.MainHand: { - var off = item.HasSubModel - ? new CharacterWeapon(item.SubModel.id, item.SubModel.type, item.SubModel.variant, actor.DrawObject.OffHand.Stain) - : item.IsBothHand + var off = item2.HasSubModel + ? new CharacterWeapon(item2.SubModel.id, item2.SubModel.type, item2.SubModel.variant, actor.DrawObject.OffHand.Stain) + : item2.IsBothHand ? CharacterWeapon.Empty : actor.OffHand; - var main = new CharacterWeapon(item.MainModel.id, item.MainModel.type, item.MainModel.variant, actor.DrawObject.MainHand.Stain); + var main = new CharacterWeapon(item2.MainModel.id, item2.MainModel.type, item2.MainModel.variant, actor.DrawObject.MainHand.Stain); Glamourer.RedrawManager.LoadWeapon(actor, main, off); return; } case EquipSlot.OffHand: { - var off = new CharacterWeapon(item.MainModel.id, item.MainModel.type, item.MainModel.variant, actor.DrawObject.OffHand.Stain); + var off = new CharacterWeapon(item2.MainModel.id, item2.MainModel.type, item2.MainModel.variant, actor.DrawObject.OffHand.Stain); var main = actor.MainHand; Glamourer.RedrawManager.LoadWeapon(actor, main, off); return; } default: { - var current = actor.DrawObject.Equip[item.EquippableTo]; - var armor = new CharacterArmor(item.MainModel.id, (byte)item.MainModel.variant, current.Stain); - Glamourer.RedrawManager.ChangeEquip(actor.DrawObject, item.EquippableTo, armor); + var current = actor.DrawObject.Equip[item2.EquippableTo]; + var armor = new CharacterArmor(item2.MainModel.id, (byte)item2.MainModel.variant, current.Stain); + Glamourer.RedrawManager.ChangeEquip(actor.DrawObject, item2.EquippableTo, armor); return; } } diff --git a/Glamourer/Dalamud.cs b/Glamourer/Dalamud.cs index 4a1b58e..65a9c5f 100644 --- a/Glamourer/Dalamud.cs +++ b/Glamourer/Dalamud.cs @@ -1,6 +1,7 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Game.Gui; @@ -18,13 +19,14 @@ public class Dalamud // @formatter:off [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; // @formatter:on } diff --git a/Glamourer/Designs/CharacterSave.cs b/Glamourer/Designs/CharacterSave.cs new file mode 100644 index 0000000..81102d7 --- /dev/null +++ b/Glamourer/Designs/CharacterSave.cs @@ -0,0 +1,88 @@ +using System.Runtime.InteropServices; +using Glamourer.Customization; +using Glamourer.Interop; +using Penumbra.GameData.Structs; +using Penumbra.String.Functions; +using CustomizeData = Penumbra.GameData.Structs.CustomizeData; + +namespace Glamourer.Designs; + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct CharacterData +{ + public uint ModelId; + public CustomizeData CustomizeData; + public CharacterWeapon MainHand; + public CharacterWeapon OffHand; + public CharacterArmor Head; + public CharacterArmor Body; + public CharacterArmor Hands; + public CharacterArmor Legs; + public CharacterArmor Feet; + public CharacterArmor Ears; + public CharacterArmor Neck; + public CharacterArmor Wrists; + public CharacterArmor RFinger; + public CharacterArmor LFinger; + + public unsafe Customize Customize + { + get + { + fixed (CustomizeData* ptr = &CustomizeData) + { + return new Customize(ptr); + } + } + } + + public unsafe CharacterEquip Equipment + { + get + { + fixed (CharacterArmor* ptr = &Head) + { + return new CharacterEquip(ptr); + } + } + } + + public static readonly CharacterData Default + = new() + { + ModelId = 0, + CustomizeData = Customize.Default, + MainHand = CharacterWeapon.Empty, + OffHand = CharacterWeapon.Empty, + Head = CharacterArmor.Empty, + Body = CharacterArmor.Empty, + Hands = CharacterArmor.Empty, + Legs = CharacterArmor.Empty, + Feet = CharacterArmor.Empty, + Ears = CharacterArmor.Empty, + Neck = CharacterArmor.Empty, + Wrists = CharacterArmor.Empty, + RFinger = CharacterArmor.Empty, + LFinger = CharacterArmor.Empty, + }; + + public readonly unsafe CharacterData Clone() + { + var data = new CharacterData(); + fixed (void* ptr = &this) + { + MemoryUtility.MemCpyUnchecked(&data, ptr, sizeof(CharacterData)); + } + + return data; + } + + public void Load(IDesignable designable) + { + ModelId = designable.ModelId; + Customize.Load(designable.Customize); + Equipment.Load(designable.Equip); + MainHand = designable.MainHand; + OffHand = designable.OffHand; + } +} diff --git a/Glamourer/Designs/Design.Manager.cs b/Glamourer/Designs/Design.Manager.cs index 6e0bdd6..ac9c529 100644 --- a/Glamourer/Designs/Design.Manager.cs +++ b/Glamourer/Designs/Design.Manager.cs @@ -2,106 +2,18 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using Dalamud.Plugin; -using ImGuizmoNET; +using Dalamud.Utility; +using Glamourer.Customization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; -using OtterGui.Filesystem; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; 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 @@ -109,7 +21,8 @@ public partial class Design public const string DesignFolderName = "designs"; public readonly string DesignFolder; - private readonly List _designs = new(); + private readonly FrameworkManager _framework; + private readonly List _designs = new(); public enum DesignChangeType { @@ -121,18 +34,54 @@ public partial class Design AddedTag, RemovedTag, ChangedTag, + Customize, + Equip, + Weapon, + Stain, + ApplyCustomize, + ApplyEquip, + Other, } - public delegate void DesignChangeDelegate(DesignChangeType type, int designIdx, string? oldData = null, string? newData = null, - int tagIdx = -1); + public delegate void DesignChangeDelegate(DesignChangeType type, Design design, object? changeData = null); - public event DesignChangeDelegate? DesignChange; + public event DesignChangeDelegate DesignChange; public IReadOnlyList Designs => _designs; - public Manager(DalamudPluginInterface pi) - => DesignFolder = SetDesignFolder(pi); + public Manager(DalamudPluginInterface pi, FrameworkManager framework) + { + _framework = framework; + DesignFolder = SetDesignFolder(pi); + DesignChange += OnChange; + LoadDesigns(); + MigrateOldDesigns(pi, Path.Combine(new DirectoryInfo(DesignFolder).Parent!.FullName, "Designs.json")); + } + + private void OnChange(DesignChangeType type, Design design, object? _) + { + switch (type) + { + case DesignChangeType.Created: + SaveDesignInternal(design); + return; + case DesignChangeType.Renamed: + case DesignChangeType.ChangedDescription: + case DesignChangeType.AddedTag: + case DesignChangeType.RemovedTag: + case DesignChangeType.ChangedTag: + case DesignChangeType.Customize: + case DesignChangeType.Equip: + case DesignChangeType.Weapon: + case DesignChangeType.Stain: + case DesignChangeType.ApplyCustomize: + case DesignChangeType.ApplyEquip: + case DesignChangeType.Other: + SaveDesign(design); + return; + } + } private static string SetDesignFolder(DalamudPluginInterface pi) { @@ -153,9 +102,12 @@ public partial class Design } private string CreateFileName(Design design) - => Path.Combine(DesignFolder, $"{design.Name.RemoveInvalidPathSymbols()}_{design.Identifier}.json"); + => Path.Combine(DesignFolder, $"{design.Identifier}.json"); public void SaveDesign(Design design) + => _framework.RegisterDelayed($"{nameof(SaveDesign)}_{design.Identifier}", () => SaveDesignInternal(design)); + + private void SaveDesignInternal(Design design) { var fileName = CreateFileName(design); try @@ -173,24 +125,54 @@ public partial class Design public void LoadDesigns() { _designs.Clear(); + List<(Design, string)> invalidNames = new(); + var skipped = 0; 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); + var design = LoadDesign(data, out var changes); + 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."); + + // TODO something when changed? design.Index = _designs.Count; _designs.Add(design); } catch (Exception ex) { Glamourer.Log.Error($"Could not load design, skipped:\n{ex}"); + ++skipped; } } - Glamourer.Log.Information($"Loaded {_designs.Count} designs."); - DesignChange?.Invoke(DesignChangeType.ReloadedAll, -1); + var failed = 0; + foreach (var (design, name) in invalidNames) + { + try + { + var correctName = CreateFileName(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}"); + } + } + + 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)}"); + DesignChange.Invoke(DesignChangeType.ReloadedAll, null!); } public Design Create(string name) @@ -198,13 +180,13 @@ public partial class Design var design = new Design() { CreationDate = DateTimeOffset.UtcNow, - Identifier = Guid.NewGuid(), + Identifier = CreateNewGuid(), Index = _designs.Count, Name = name, }; _designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier}."); - DesignChange?.Invoke(DesignChangeType.Created, design.Index); + DesignChange.Invoke(DesignChangeType.Created, design); return design; } @@ -218,7 +200,7 @@ public partial class Design { File.Delete(fileName); Glamourer.Log.Debug($"Deleted design {design.Identifier}."); - DesignChange?.Invoke(DesignChangeType.Deleted, design.Index); + DesignChange.Invoke(DesignChangeType.Deleted, design); } catch (Exception ex) { @@ -228,7 +210,7 @@ public partial class Design public void Rename(Design design, string newName) { - var oldName = design.Name; + var oldName = design.Name.Text; var oldFileName = CreateFileName(design); if (File.Exists(oldFileName)) try @@ -242,18 +224,15 @@ public partial class Design } design.Name = newName; - SaveDesign(design); Glamourer.Log.Debug($"Renamed design {design.Identifier}."); - DesignChange?.Invoke(DesignChangeType.Renamed, design.Index, oldName, newName); + DesignChange.Invoke(DesignChangeType.Renamed, design, oldName); } 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); + Glamourer.Log.Debug($"Changed description of design {design.Identifier}."); + DesignChange.Invoke(DesignChangeType.ChangedDescription, design); } public void AddTag(Design design, string tag) @@ -263,9 +242,8 @@ public partial class Design 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); + Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}."); + DesignChange.Invoke(DesignChangeType.AddedTag, design); } public void RemoveTag(Design design, string tag) @@ -279,9 +257,8 @@ public partial class Design { 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); + Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}."); + DesignChange.Invoke(DesignChangeType.RemovedTag, design); } @@ -294,8 +271,156 @@ public partial class Design 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); + Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags."); + DesignChange.Invoke(DesignChangeType.ChangedTag, design); + } + + public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value) + { + var old = design.GetCustomize(idx); + if (design.SetCustomize(idx, value)) + { + Glamourer.Log.Debug($"Changed customize {idx} in design {design.Identifier} from {old.Value} to {value.Value}"); + DesignChange.Invoke(DesignChangeType.Customize, design, idx); + } + } + + public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) + { + if (design.SetApplyCustomize(idx, value)) + { + Glamourer.Log.Debug($"Set applying of customization {idx} to {value}."); + DesignChange.Invoke(DesignChangeType.ApplyCustomize, design, idx); + } + } + + public void ChangeEquip(Design design, EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) + { + var old = design.Armor(slot); + if (design.SetArmor(slot, itemId, item)) + { + var n = design.Armor(slot); + Glamourer.Log.Debug( + $"Set {slot} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId})."); + DesignChange.Invoke(DesignChangeType.Equip, design, slot); + } + } + + public void ChangeWeapon(Design design, uint itemId, EquipSlot offhand, Lumina.Excel.GeneratedSheets.Item? item = null) + { + var (old, change, n) = offhand == EquipSlot.OffHand + ? (design.WeaponOff, design.SetOffhand(itemId, item), design.WeaponOff) + : (design.WeaponMain, design.SetMainhand(itemId, item), design.WeaponMain); + if (change) + { + Glamourer.Log.Debug( + $"Set {offhand} weapon in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId})."); + DesignChange.Invoke(DesignChangeType.Weapon, design, offhand); + } + } + + public void ChangeApplyEquip(Design design, EquipSlot slot, bool value) + { + if (design.SetApplyEquip(slot, value)) + { + Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); + DesignChange.Invoke(DesignChangeType.ApplyEquip, design, slot); + } + } + + public void ChangeStain(Design design, EquipSlot slot, StainId stain) + { + if (design.SetStain(slot, stain)) + { + Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}."); + DesignChange.Invoke(DesignChangeType.Stain, design, slot); + } + } + + public void ChangeApplyStain(Design design, EquipSlot slot, bool value) + { + if (design.SetApplyStain(slot, value)) + { + Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); + DesignChange.Invoke(DesignChangeType.Stain, design, slot); + } + } + + private Guid CreateNewGuid() + { + while (true) + { + var guid = Guid.NewGuid(); + if (_designs.All(d => d.Identifier != guid)) + return guid; + } + } + + 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); + DesignChange.Invoke(DesignChangeType.Created, design); + return true; + } + + private void MigrateOldDesigns(DalamudPluginInterface pi, string filePath) + { + if (!File.Exists(filePath)) + return; + + var errors = 0; + var successes = 0; + try + { + var text = File.ReadAllText(filePath); + 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() + { + CreationDate = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Name = actualName, + }; + design.MigrateBase64(base64); + Add(design, $"Migrated old design to {design.Identifier}."); + migratedFileSystemPaths.Add(design.Identifier.ToString(), name); + ++successes; + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not migrate design {name}:\n{ex}"); + ++errors; + } + } + + DesignFileSystem.MigrateOldPaths(pi, migratedFileSystemPaths); + Glamourer.Log.Information($"Successfully migrated {successes} old designs. Failed to migrate {errors} designs."); + } + catch (Exception e) + { + Glamourer.Log.Error($"Could not migrate old design file {filePath}:\n{e}"); + } + + try + { + File.Move(filePath, Path.ChangeExtension(filePath, ".json.bak")); + Glamourer.Log.Information($"Moved migrated design file {filePath} to backup file."); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not move migrated design file {filePath} to backup file:\n{ex}"); + } } } } diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index d7a5e96..4285997 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -1,44 +1,339 @@ using System; using System.Linq; +using Glamourer.Customization; +using Glamourer.Util; using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Designs; -public partial class Design +public partial class Design : DesignBase { + public const int FileVersion = 1; + public Guid Identifier { get; private init; } public DateTimeOffset CreationDate { get; private init; } - public string Name { get; private set; } = string.Empty; + public LowerString Name { get; private set; } = LowerString.Empty; public string Description { get; private set; } = string.Empty; public string[] Tags { get; private set; } = Array.Empty(); public int Index { get; private set; } + private EquipFlag _applyEquip; + private CustomizeFlag _applyCustomize; + public QuadBool Wetness { get; private set; } = QuadBool.NullFalse; + public QuadBool Visor { get; private set; } = QuadBool.NullFalse; + public QuadBool Hat { get; private set; } = QuadBool.NullFalse; + public QuadBool Weapon { get; private set; } = QuadBool.NullFalse; + public bool WriteProtected { get; private set; } + + public bool DoApplyEquip(EquipSlot slot) + => _applyEquip.HasFlag(slot.ToFlag()); + + public bool DoApplyStain(EquipSlot slot) + => _applyEquip.HasFlag(slot.ToStainFlag()); + + public bool DoApplyCustomize(CustomizeIndex idx) + => _applyCustomize.HasFlag(idx.ToFlag()); + + private bool SetApplyEquip(EquipSlot slot, bool value) + { + var newValue = value ? _applyEquip | slot.ToFlag() : _applyEquip & ~slot.ToFlag(); + if (newValue == _applyEquip) + return false; + + _applyEquip = newValue; + return true; + } + + private bool SetApplyStain(EquipSlot slot, bool value) + { + var newValue = value ? _applyEquip | slot.ToStainFlag() : _applyEquip & ~slot.ToStainFlag(); + if (newValue == _applyEquip) + return false; + + _applyEquip = newValue; + return true; + } + + private bool SetApplyCustomize(CustomizeIndex idx, bool value) + { + var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag(); + if (newValue == _applyCustomize) + return false; + + _applyCustomize = newValue; + return true; + } + + + private Design() + { } + public JObject JsonSerialize() { var ret = new JObject { - [nameof(Identifier)] = Identifier, - [nameof(CreationDate)] = CreationDate, - [nameof(Name)] = Name, - [nameof(Description)] = Description, - [nameof(Tags)] = JArray.FromObject(Tags), + [nameof(FileVersion)] = FileVersion, + [nameof(Identifier)] = Identifier, + [nameof(CreationDate)] = CreationDate, + [nameof(Name)] = Name.Text, + [nameof(Description)] = Description, + [nameof(Tags)] = JArray.FromObject(Tags), + [nameof(WriteProtected)] = WriteProtected, + [nameof(CharacterData.Equipment)] = SerializeEquipment(), + [nameof(CharacterData.Customize)] = SerializeCustomize(), }; return ret; } - public static Design LoadDesign(JObject json) - => new() + public JObject SerializeEquipment() + { + static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain) + => new() + { + [nameof(Item.ItemId)] = itemId, + [nameof(Item.Stain)] = stain.Value, + ["Apply"] = apply, + ["ApplyStain"] = applyStain, + }; + + var ret = new JObject() { - 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, + [nameof(MainHand)] = + Serialize(MainHand, CharacterData.MainHand.Stain, DoApplyEquip(EquipSlot.MainHand), DoApplyStain(EquipSlot.MainHand)), + [nameof(OffHand)] = Serialize(OffHand, CharacterData.OffHand.Stain, DoApplyEquip(EquipSlot.OffHand), DoApplyStain(EquipSlot.OffHand)), + }; + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var armor = Armor(slot); + ret[slot.ToString()] = Serialize(armor.ItemId, armor.Stain, DoApplyEquip(slot), DoApplyStain(slot)); + } + + ret[nameof(Hat)] = Hat.ToJObject("Show", "Apply"); + ret[nameof(Weapon)] = Weapon.ToJObject("Show", "Apply"); + ret[nameof(Visor)] = Visor.ToJObject("IsToggled", "Apply"); + + return ret; + } + + public JObject SerializeCustomize() + { + var ret = new JObject() + { + [nameof(ModelId)] = ModelId, + }; + var customize = CharacterData.Customize; + foreach (var idx in Enum.GetValues()) + { + var data = customize[idx]; + ret[idx.ToString()] = new JObject() + { + ["Value"] = data.Value, + ["Apply"] = true, + }; + } + + ret[nameof(Wetness)] = Wetness.ToJObject("IsWet", "Apply"); + return ret; + } + + public static Design LoadDesign(JObject json, out bool changes) + { + var version = json[nameof(FileVersion)]?.ToObject() ?? 0; + return version switch + { + 1 => LoadDesignV1(json, out changes), + _ => throw new Exception("The design to be loaded has no valid Version."), + }; + } + + private static Design LoadDesignV1(JObject json, out bool changes) + { + static string[] ParseTags(JObject json) + { + var tags = json["Tags"]?.ToObject() ?? Array.Empty(); + return tags.OrderBy(t => t).Distinct().ToArray(); + } + + var design = new Design() + { + CreationDate = json["CreationDate"]?.ToObject() ?? throw new ArgumentNullException("CreationDate"), + Identifier = json["Identifier"]?.ToObject() ?? throw new ArgumentNullException("Identifier"), + Name = new LowerString(json["Name"]?.ToObject() ?? throw new ArgumentNullException("Name")), + Description = json["Description"]?.ToObject() ?? string.Empty, Tags = ParseTags(json), }; - private static string[] ParseTags(JObject json) + changes = LoadEquip(json["Equipment"], design); + changes |= LoadCustomize(json["Customize"], design); + return design; + } + + private static bool LoadEquip(JToken? equip, Design design) { - var tags = json[nameof(Tags)]?.ToObject() ?? Array.Empty(); - return tags.OrderBy(t => t).Distinct().ToArray(); + if (equip == null) + return true; + + static (uint, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item) + { + var id = item?["ItemId"]?.ToObject() ?? ItemManager.NothingId(slot); + var stain = (StainId)(item?["Stain"]?.ToObject() ?? 0); + var apply = item?["Apply"]?.ToObject() ?? false; + var applyStain = item?["ApplyStain"]?.ToObject() ?? false; + return (id, stain, apply, applyStain); + } + + var changes = false; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]); + changes |= !design.SetArmor(slot, id); + changes |= !design.SetStain(slot, stain); + design.SetApplyEquip(slot, apply); + design.SetApplyStain(slot, applyStain); + } + + var main = equip["MainHand"]; + if (main == null) + { + changes = true; + } + else + { + var id = main["ItemId"]?.ToObject() ?? Glamourer.Items.DefaultSword.RowId; + var stain = (StainId)(main["Stain"]?.ToObject() ?? 0); + var apply = main["Apply"]?.ToObject() ?? false; + var applyStain = main["ApplyStain"]?.ToObject() ?? false; + changes |= !design.SetMainhand(id); + changes |= !design.SetStain(EquipSlot.MainHand, stain); + design.SetApplyEquip(EquipSlot.MainHand, apply); + design.SetApplyStain(EquipSlot.MainHand, applyStain); + } + + var off = equip["OffHand"]; + if (off == null) + { + changes = true; + } + else + { + var id = off["ItemId"]?.ToObject() ?? ItemManager.NothingId(design.MainhandType.Offhand()); + var stain = (StainId)(off["Stain"]?.ToObject() ?? 0); + var apply = off["Apply"]?.ToObject() ?? false; + var applyStain = off["ApplyStain"]?.ToObject() ?? false; + changes |= !design.SetOffhand(id); + changes |= !design.SetStain(EquipSlot.OffHand, stain); + design.SetApplyEquip(EquipSlot.OffHand, apply); + design.SetApplyStain(EquipSlot.OffHand, applyStain); + } + + design.Hat = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse); + design.Weapon = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse); + design.Visor = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); + + return changes; + } + + private static bool LoadCustomize(JToken? json, Design design) + { + if (json == null) + return true; + + var customize = design.CharacterData.Customize; + foreach (var idx in Enum.GetValues()) + { + var tok = json[idx.ToString()]; + var data = (CustomizeValue)(tok?["Value"]?.ToObject() ?? 0); + var apply = tok?["Apply"]?.ToObject() ?? false; + customize[idx] = data; + design.SetApplyCustomize(idx, apply); + } + + design.Wetness = QuadBool.FromJObject(json["Wetness"], "IsWet", "Apply", QuadBool.NullFalse); + + return false; + } + + + public void MigrateBase64(string base64) + { + static void CheckSize(int length, int requiredLength) + { + if (length != requiredLength) + throw new Exception( + $"Can not parse Base64 string into CharacterSave:\n\tInvalid size {length} instead of {requiredLength}."); + } + + var bytes = Convert.FromBase64String(base64); + + byte applicationFlags; + ushort equipFlags; + + switch (bytes[0]) + { + case 1: + { + CheckSize(bytes.Length, 86); + applicationFlags = bytes[1]; + equipFlags = BitConverter.ToUInt16(bytes, 2); + break; + } + case 2: + { + CheckSize(bytes.Length, 91); + applicationFlags = bytes[1]; + equipFlags = BitConverter.ToUInt16(bytes, 2); + Hat = Hat.SetValue((bytes[90] & 0x01) == 0); + Visor = Visor.SetValue((bytes[90] & 0x10) != 0); + Weapon = Weapon.SetValue((bytes[90] & 0x02) == 0); + break; + } + default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}."); + } + + _applyCustomize = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0; + Wetness = (applicationFlags & 0x02) != 0 ? QuadBool.True : QuadBool.NullFalse; + Hat = Hat.SetEnabled((applicationFlags & 0x04) != 0); + Weapon = Weapon.SetEnabled((applicationFlags & 0x08) != 0); + Visor = Visor.SetEnabled((applicationFlags & 0x10) != 0); + WriteProtected = (applicationFlags & 0x20) != 0; + + CharacterData.ModelId = 0; + + SetApplyEquip(EquipSlot.MainHand, (equipFlags & 0x0001) != 0); + SetApplyEquip(EquipSlot.OffHand, (equipFlags & 0x0002) != 0); + SetApplyStain(EquipSlot.MainHand, (equipFlags & 0x0001) != 0); + SetApplyStain(EquipSlot.OffHand, (equipFlags & 0x0002) != 0); + var flag = 0x0002u; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + flag <<= 1; + var apply = (equipFlags & flag) != 0; + SetApplyEquip(slot, apply); + SetApplyStain(slot, apply); + } + unsafe + { + fixed (byte* ptr = bytes) + { + CharacterData.CustomizeData.Read(ptr + 4); + var cur = (CharacterWeapon*)(ptr + 30); + + UpdateMainhand(cur[0]); + SetStain(EquipSlot.MainHand, cur[0].Stain); + UpdateOffhand(cur[1]); + SetStain(EquipSlot.OffHand, cur[1].Stain); + var eq = (CharacterArmor*)(cur + 2); + foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex()) + { + UpdateArmor(slot, eq[idx], true); + SetStain(slot, eq[idx].Stain); + } + } + } } } diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs new file mode 100644 index 0000000..fc42c3e --- /dev/null +++ b/Glamourer/Designs/DesignBase.cs @@ -0,0 +1,330 @@ +using System; +using Glamourer.Customization; +using Glamourer.Util; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public class DesignBase +{ + protected CharacterData CharacterData = CharacterData.Default; + public FullEquipType MainhandType { get; protected set; } + + public uint Head { get; protected set; } = ItemManager.NothingId(EquipSlot.Head); + public uint Body { get; protected set; } = ItemManager.NothingId(EquipSlot.Body); + public uint Hands { get; protected set; } = ItemManager.NothingId(EquipSlot.Hands); + public uint Legs { get; protected set; } = ItemManager.NothingId(EquipSlot.Legs); + public uint Feet { get; protected set; } = ItemManager.NothingId(EquipSlot.Feet); + public uint Ears { get; protected set; } = ItemManager.NothingId(EquipSlot.Ears); + public uint Neck { get; protected set; } = ItemManager.NothingId(EquipSlot.Neck); + public uint Wrists { get; protected set; } = ItemManager.NothingId(EquipSlot.Wrists); + public uint RFinger { get; protected set; } = ItemManager.NothingId(EquipSlot.RFinger); + public uint LFinger { get; protected set; } = ItemManager.NothingId(EquipSlot.RFinger); + public uint MainHand { get; protected set; } + public uint OffHand { get; protected set; } + + public string HeadName { get; protected set; } = ItemManager.Nothing; + public string BodyName { get; protected set; } = ItemManager.Nothing; + public string HandsName { get; protected set; } = ItemManager.Nothing; + public string LegsName { get; protected set; } = ItemManager.Nothing; + public string FeetName { get; protected set; } = ItemManager.Nothing; + public string EarsName { get; protected set; } = ItemManager.Nothing; + public string NeckName { get; protected set; } = ItemManager.Nothing; + public string WristsName { get; protected set; } = ItemManager.Nothing; + public string RFingerName { get; protected set; } = ItemManager.Nothing; + public string LFingerName { get; protected set; } = ItemManager.Nothing; + public string MainhandName { get; protected set; } + public string OffhandName { get; protected set; } + + public Customize Customize() + => CharacterData.Customize; + + public CharacterEquip Equipment() + => CharacterData.Equipment; + + public DesignBase() + { + MainHand = Glamourer.Items.DefaultSword.RowId; + (_, CharacterData.MainHand.Set, CharacterData.MainHand.Type, CharacterData.MainHand.Variant, MainhandName, MainhandType) = + Glamourer.Items.Resolve(MainHand, Glamourer.Items.DefaultSword); + OffHand = ItemManager.NothingId(MainhandType.Offhand()); + (_, CharacterData.OffHand.Set, CharacterData.OffHand.Type, CharacterData.OffHand.Variant, OffhandName, _) = + Glamourer.Items.Resolve(OffHand, MainhandType); + } + + public uint ModelId + => CharacterData.ModelId; + + public Item Armor(EquipSlot slot) + { + return slot switch + { + EquipSlot.Head => new Item(HeadName, Head, CharacterData.Head), + EquipSlot.Body => new Item(BodyName, Body, CharacterData.Body), + EquipSlot.Hands => new Item(HandsName, Hands, CharacterData.Hands), + EquipSlot.Legs => new Item(LegsName, Legs, CharacterData.Legs), + EquipSlot.Feet => new Item(FeetName, Feet, CharacterData.Feet), + EquipSlot.Ears => new Item(EarsName, Ears, CharacterData.Ears), + EquipSlot.Neck => new Item(NeckName, Neck, CharacterData.Neck), + EquipSlot.Wrists => new Item(WristsName, Wrists, CharacterData.Wrists), + EquipSlot.RFinger => new Item(RFingerName, RFinger, CharacterData.RFinger), + EquipSlot.LFinger => new Item(LFingerName, LFinger, CharacterData.LFinger), + _ => throw new Exception("Invalid equip slot for item."), + }; + } + + public Weapon WeaponMain + => new(MainhandName, MainHand, CharacterData.MainHand, MainhandType); + + public Weapon WeaponOff + => Designs.Weapon.Offhand(OffhandName, OffHand, CharacterData.OffHand, MainhandType); + + public CustomizeValue GetCustomize(CustomizeIndex idx) + => Customize()[idx]; + + protected bool SetCustomize(CustomizeIndex idx, CustomizeValue value) + { + var c = Customize(); + if (c[idx] == value) + return false; + + c[idx] = value; + return true; + } + + protected bool SetArmor(EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) + { + var (valid, set, variant, name) = Glamourer.Items.Resolve(slot, itemId, item); + if (!valid) + return false; + + return SetArmor(slot, set, variant, name, itemId); + } + + protected bool UpdateArmor(EquipSlot slot, CharacterArmor armor, bool force) + { + if (!force) + { + switch (slot) + { + case EquipSlot.Head when CharacterData.Head.Value == armor.Value: return false; + case EquipSlot.Body when CharacterData.Body.Value == armor.Value: return false; + case EquipSlot.Hands when CharacterData.Hands.Value == armor.Value: return false; + case EquipSlot.Legs when CharacterData.Legs.Value == armor.Value: return false; + case EquipSlot.Feet when CharacterData.Feet.Value == armor.Value: return false; + case EquipSlot.Ears when CharacterData.Ears.Value == armor.Value: return false; + case EquipSlot.Neck when CharacterData.Neck.Value == armor.Value: return false; + case EquipSlot.Wrists when CharacterData.Wrists.Value == armor.Value: return false; + case EquipSlot.RFinger when CharacterData.RFinger.Value == armor.Value: return false; + case EquipSlot.LFinger when CharacterData.LFinger.Value == armor.Value: return false; + } + } + + var (valid, id, name) = Glamourer.Items.Identify(slot, armor.Set, armor.Variant); + if (!valid) + return false; + + return SetArmor(slot, armor.Set, armor.Variant, name, id); + } + + protected bool SetMainhand(uint mainId, Lumina.Excel.GeneratedSheets.Item? main = null) + { + if (mainId == MainHand) + return false; + + var (valid, set, weapon, variant, name, type) = Glamourer.Items.Resolve(mainId, main); + if (!valid) + return false; + + var fixOffhand = type.Offhand() != MainhandType.Offhand(); + + MainHand = mainId; + MainhandName = name; + MainhandType = type; + CharacterData.MainHand.Set = set; + CharacterData.MainHand.Type = weapon; + CharacterData.MainHand.Variant = variant; + if (fixOffhand) + SetOffhand(ItemManager.NothingId(type.Offhand())); + return true; + } + + protected bool SetOffhand(uint offId, Lumina.Excel.GeneratedSheets.Item? off = null) + { + if (offId == OffHand) + return false; + + var (valid, set, weapon, variant, name, type) = Glamourer.Items.Resolve(offId, MainhandType, off); + if (!valid) + return false; + + OffHand = offId; + OffhandName = name; + CharacterData.OffHand.Set = set; + CharacterData.OffHand.Type = weapon; + CharacterData.OffHand.Variant = variant; + return true; + } + + protected bool UpdateMainhand(CharacterWeapon weapon) + { + if (weapon.Value == CharacterData.MainHand.Value) + return false; + + var (valid, id, name, type) = Glamourer.Items.Identify(EquipSlot.MainHand, weapon.Set, weapon.Type, (byte)weapon.Variant); + if (!valid || id == MainHand) + return false; + + var fixOffhand = type.Offhand() != MainhandType.Offhand(); + + MainHand = id; + MainhandName = name; + MainhandType = type; + CharacterData.MainHand.Set = weapon.Set; + CharacterData.MainHand.Type = weapon.Type; + CharacterData.MainHand.Variant = weapon.Variant; + CharacterData.MainHand.Stain = weapon.Stain; + if (fixOffhand) + SetOffhand(ItemManager.NothingId(type.Offhand())); + return true; + } + + protected bool UpdateOffhand(CharacterWeapon weapon) + { + if (weapon.Value == CharacterData.OffHand.Value) + return false; + + var (valid, id, name, _) = Glamourer.Items.Identify(EquipSlot.OffHand, weapon.Set, weapon.Type, (byte)weapon.Variant, MainhandType); + if (!valid || id == OffHand) + return false; + + OffHand = id; + OffhandName = name; + CharacterData.OffHand.Set = weapon.Set; + CharacterData.OffHand.Type = weapon.Type; + CharacterData.OffHand.Variant = weapon.Variant; + CharacterData.OffHand.Stain = weapon.Stain; + return true; + } + + protected bool SetStain(EquipSlot slot, StainId id) + { + return slot switch + { + EquipSlot.MainHand => SetIfDifferent(ref CharacterData.MainHand.Stain, id), + EquipSlot.OffHand => SetIfDifferent(ref CharacterData.OffHand.Stain, id), + EquipSlot.Head => SetIfDifferent(ref CharacterData.Head.Stain, id), + EquipSlot.Body => SetIfDifferent(ref CharacterData.Body.Stain, id), + EquipSlot.Hands => SetIfDifferent(ref CharacterData.Hands.Stain, id), + EquipSlot.Legs => SetIfDifferent(ref CharacterData.Legs.Stain, id), + EquipSlot.Feet => SetIfDifferent(ref CharacterData.Feet.Stain, id), + EquipSlot.Ears => SetIfDifferent(ref CharacterData.Ears.Stain, id), + EquipSlot.Neck => SetIfDifferent(ref CharacterData.Neck.Stain, id), + EquipSlot.Wrists => SetIfDifferent(ref CharacterData.Wrists.Stain, id), + EquipSlot.RFinger => SetIfDifferent(ref CharacterData.RFinger.Stain, id), + EquipSlot.LFinger => SetIfDifferent(ref CharacterData.LFinger.Stain, id), + _ => false, + }; + } + + protected static bool SetIfDifferent(ref T old, T value) where T : IEquatable + { + if (old.Equals(value)) + return false; + + old = value; + return true; + } + + + private bool SetArmor(EquipSlot slot, SetId set, byte variant, string name, uint id) + { + var changes = false; + switch (slot) + { + case EquipSlot.Head: + changes |= SetIfDifferent(ref CharacterData.Head.Set, set); + changes |= SetIfDifferent(ref CharacterData.Head.Variant, variant); + changes |= HeadName != name; + HeadName = name; + changes |= Head != id; + Head = id; + return changes; + case EquipSlot.Body: + changes |= SetIfDifferent(ref CharacterData.Body.Set, set); + changes |= SetIfDifferent(ref CharacterData.Body.Variant, variant); + changes |= BodyName != name; + BodyName = name; + changes |= Body != id; + Body = id; + return changes; + case EquipSlot.Hands: + changes |= SetIfDifferent(ref CharacterData.Hands.Set, set); + changes |= SetIfDifferent(ref CharacterData.Hands.Variant, variant); + changes |= HandsName != name; + HandsName = name; + changes |= Hands != id; + Hands = id; + return changes; + case EquipSlot.Legs: + changes |= SetIfDifferent(ref CharacterData.Legs.Set, set); + changes |= SetIfDifferent(ref CharacterData.Legs.Variant, variant); + changes |= LegsName != name; + LegsName = name; + changes |= Legs != id; + Legs = id; + return changes; + case EquipSlot.Feet: + changes |= SetIfDifferent(ref CharacterData.Feet.Set, set); + changes |= SetIfDifferent(ref CharacterData.Feet.Variant, variant); + changes |= FeetName != name; + FeetName = name; + changes |= Feet != id; + Feet = id; + return changes; + case EquipSlot.Ears: + changes |= SetIfDifferent(ref CharacterData.Ears.Set, set); + changes |= SetIfDifferent(ref CharacterData.Ears.Variant, variant); + changes |= EarsName != name; + EarsName = name; + changes |= Ears != id; + Ears = id; + return changes; + case EquipSlot.Neck: + changes |= SetIfDifferent(ref CharacterData.Neck.Set, set); + changes |= SetIfDifferent(ref CharacterData.Neck.Variant, variant); + changes |= NeckName != name; + NeckName = name; + changes |= Neck != id; + Neck = id; + return changes; + case EquipSlot.Wrists: + changes |= SetIfDifferent(ref CharacterData.Wrists.Set, set); + changes |= SetIfDifferent(ref CharacterData.Wrists.Variant, variant); + changes |= WristsName != name; + WristsName = name; + changes |= Wrists != id; + Wrists = id; + return changes; + case EquipSlot.RFinger: + changes |= SetIfDifferent(ref CharacterData.RFinger.Set, set); + changes |= SetIfDifferent(ref CharacterData.RFinger.Variant, variant); + changes |= RFingerName != name; + RFingerName = name; + changes |= RFinger != id; + RFinger = id; + return changes; + case EquipSlot.LFinger: + changes |= SetIfDifferent(ref CharacterData.LFinger.Set, set); + changes |= SetIfDifferent(ref CharacterData.LFinger.Variant, variant); + changes |= LFingerName != name; + LFingerName = name; + changes |= LFinger != id; + LFinger = id; + return changes; + default: return false; + } + } +} diff --git a/Glamourer/Designs/DesignFileSystem.cs b/Glamourer/Designs/DesignFileSystem.cs new file mode 100644 index 0000000..a94dee7 --- /dev/null +++ b/Glamourer/Designs/DesignFileSystem.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Dalamud.Plugin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; +using OtterGui.Filesystem; + +namespace Glamourer.Designs; + +public sealed class DesignFileSystem : FileSystem, IDisposable +{ + public static string GetDesignFileSystemFile(DalamudPluginInterface pi) + => Path.Combine(pi.GetPluginConfigDirectory(), "sort_order.json"); + + public readonly string DesignFileSystemFile; + private readonly FrameworkManager _framework; + private readonly Design.Manager _designManager; + + public DesignFileSystem(Design.Manager designManager, DalamudPluginInterface pi, FrameworkManager framework) + { + DesignFileSystemFile = GetDesignFileSystemFile(pi); + _designManager = designManager; + _framework = framework; + _designManager.DesignChange += OnDataChange; + Changed += OnChange; + Reload(); + } + + private void Reload() + { + if (Load(new FileInfo(DesignFileSystemFile), _designManager.Designs, DesignToIdentifier, DesignToName)) + SaveFilesystem(); + + Glamourer.Log.Debug("Reloaded design filesystem."); + } + + public void Dispose() + { + _designManager.DesignChange -= OnDataChange; + } + + 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."); + } + + public void Save() + => _framework.RegisterDelayed(nameof(SaveFilesystem), SaveFilesystem); + + private void OnDataChange(Design.Manager.DesignChangeType type, Design design, object? data) + { + switch (type) + { + case Design.Manager.DesignChangeType.Created: + var originalName = design.Name.Text.FixName(); + var name = originalName; + var counter = 1; + while (Find(name, out _)) + name = $"{originalName} ({++counter})"; + + CreateLeaf(Root, name, design); + break; + case Design.Manager.DesignChangeType.Deleted: + if (FindLeaf(design, out var leaf)) + Delete(leaf); + break; + case Design.Manager.DesignChangeType.ReloadedAll: + Reload(); + break; + case Design.Manager.DesignChangeType.Renamed when data is string oldName: + var old = oldName.FixName(); + if (Find(old, out var child) && child is not Folder) + Rename(child, design.Name); + break; + } + } + + // Used for saving and loading. + private static string DesignToIdentifier(Design design) + => design.Identifier.ToString(); + + private static string DesignToName(Design design) + => design.Name.Text.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) + : (DesignToIdentifier(design), true); + + // Search the entire filesystem for the leaf corresponding to a design. + public bool FindLeaf(Design design, [NotNullWhen(true)] out Leaf? leaf) + { + leaf = Root.GetAllDescendants(ISortMode.Lexicographical) + .OfType() + .FirstOrDefault(l => l.Value == design); + return leaf != null; + } + + internal static void MigrateOldPaths(DalamudPluginInterface pi, Dictionary oldPaths) + { + if (oldPaths.Count == 0) + return; + + var file = GetDesignFileSystemFile(pi); + try + { + JObject jObject; + if (File.Exists(file)) + { + var text = File.ReadAllText(file); + jObject = JObject.Parse(text); + var dict = jObject["Data"]?.ToObject>(); + if (dict != null) + foreach (var (key, value) in dict) + oldPaths.TryAdd(key, value); + + jObject["Data"] = JToken.FromObject(oldPaths); + } + else + { + jObject = new JObject + { + ["Data"] = JToken.FromObject(oldPaths), + ["EmptyFolders"] = JToken.FromObject(Array.Empty()), + }; + } + + var data = jObject.ToString(Formatting.Indented); + File.WriteAllText(file, data); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not migrate old folder paths to new version:\n{ex}"); + } + } +} diff --git a/Glamourer/Designs/EquipFlag.cs b/Glamourer/Designs/EquipFlag.cs new file mode 100644 index 0000000..e6a3a06 --- /dev/null +++ b/Glamourer/Designs/EquipFlag.cs @@ -0,0 +1,72 @@ +using System; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs; + +[Flags] +public enum EquipFlag : uint +{ + Head = 0x00000001, + Body = 0x00000002, + Hands = 0x00000004, + Legs = 0x00000008, + Feet = 0x00000010, + Ears = 0x00000020, + Neck = 0x00000040, + Wrist = 0x00000080, + RFinger = 0x00000100, + LFinger = 0x00000200, + Mainhand = 0x00000400, + Offhand = 0x00000800, + HeadStain = 0x00001000, + BodyStain = 0x00002000, + HandsStain = 0x00004000, + LegsStain = 0x00008000, + FeetStain = 0x00010000, + EarsStain = 0x00020000, + NeckStain = 0x00040000, + WristStain = 0x00080000, + RFingerStain = 0x00100000, + LFingerStain = 0x00200000, + MainhandStain = 0x00400000, + OffhandStain = 0x00800000, +} + +public static class EquipFlagExtensions +{ + public static EquipFlag ToFlag(this EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => EquipFlag.Mainhand, + EquipSlot.OffHand => EquipFlag.Offhand, + EquipSlot.Head => EquipFlag.Head, + EquipSlot.Body => EquipFlag.Body, + EquipSlot.Hands => EquipFlag.Hands, + EquipSlot.Legs => EquipFlag.Legs, + EquipSlot.Feet => EquipFlag.Feet, + EquipSlot.Ears => EquipFlag.Ears, + EquipSlot.Neck => EquipFlag.Neck, + EquipSlot.Wrists => EquipFlag.Wrist, + EquipSlot.RFinger => EquipFlag.RFinger, + EquipSlot.LFinger => EquipFlag.LFinger, + _ => 0, + }; + + public static EquipFlag ToStainFlag(this EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => EquipFlag.MainhandStain, + EquipSlot.OffHand => EquipFlag.OffhandStain, + EquipSlot.Head => EquipFlag.HeadStain, + EquipSlot.Body => EquipFlag.BodyStain, + EquipSlot.Hands => EquipFlag.HandsStain, + EquipSlot.Legs => EquipFlag.LegsStain, + EquipSlot.Feet => EquipFlag.FeetStain, + EquipSlot.Ears => EquipFlag.EarsStain, + EquipSlot.Neck => EquipFlag.NeckStain, + EquipSlot.Wrists => EquipFlag.WristStain, + EquipSlot.RFinger => EquipFlag.RFingerStain, + EquipSlot.LFinger => EquipFlag.LFingerStain, + _ => 0, + }; +} diff --git a/Glamourer/Designs/RevertableDesigns.cs b/Glamourer/Designs/RevertableDesigns.cs deleted file mode 100644 index 513d43a..0000000 --- a/Glamourer/Designs/RevertableDesigns.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Game.ClientState.Objects.Types; -using Glamourer.State; - -namespace Glamourer.Designs; - -public class RevertableDesigns -{ - public readonly Dictionary Saves = new(); - - public bool Add(Character actor) - { - //var name = actor.Name.ToString(); - //if (Saves.TryGetValue(name, out var save)) - // return false; - // - //save = new CharacterSave(); - //save.LoadCharacter(actor); - //Saves[name] = save; - return true; - } - - public bool RevertByNameWithoutApplication(string actorName) - { - if (!Saves.ContainsKey(actorName)) - return false; - - Saves.Remove(actorName); - return true; - } - - public bool Revert(Character actor) - { - //if (!Saves.TryGetValue(actor.Name.ToString(), out var save)) - // return false; - // - //save.Apply(actor); - //Saves.Remove(actor.Name.ToString()); - return true; - } -} diff --git a/Glamourer/Designs/Structs.cs b/Glamourer/Designs/Structs.cs new file mode 100644 index 0000000..4cc2ce5 --- /dev/null +++ b/Glamourer/Designs/Structs.cs @@ -0,0 +1,72 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public readonly struct Item +{ + public readonly string Name; + public readonly uint ItemId; + public readonly CharacterArmor Model; + + public SetId ModelBase + => Model.Set; + + public byte Variant + => Model.Variant; + + public StainId Stain + => Model.Stain; + + + public Item(string name, uint itemId, CharacterArmor armor) + { + Name = name; + ItemId = itemId; + Model.Set = armor.Set; + Model.Variant = armor.Variant; + Model.Stain = armor.Stain; + } +} + +public readonly struct Weapon +{ + public readonly string Name = string.Empty; + public readonly uint ItemId; + public readonly FullEquipType Type; + public readonly bool Valid; + public readonly CharacterWeapon Model; + + public SetId ModelBase + => Model.Set; + + public WeaponType WeaponBase + => Model.Type; + + public byte Variant + => (byte)Model.Variant; + + public StainId Stain + => Model.Stain; + + + public Weapon(string name, uint itemId, CharacterWeapon weapon, FullEquipType type) + { + Name = name; + ItemId = itemId; + Type = type; + Valid = true; + Model.Set = weapon.Set; + Model.Type = weapon.Type; + Model.Variant = (byte)weapon.Variant; + Model.Stain = weapon.Stain; + } + + public static Weapon Offhand(string name, uint itemId, CharacterWeapon weapon, FullEquipType type) + { + var offType = type.Offhand(); + return offType is FullEquipType.Unknown + ? new Weapon() + : new Weapon(name, itemId, weapon, offType); + } +} diff --git a/Glamourer/Fixed/FixedCondition.cs b/Glamourer/Fixed/FixedCondition.cs new file mode 100644 index 0000000..e864ada --- /dev/null +++ b/Glamourer/Fixed/FixedCondition.cs @@ -0,0 +1,34 @@ +using Glamourer.Interop; +using Glamourer.Structs; + +namespace Glamourer.Fixed; + +public struct FixedCondition +{ + private const ulong _territoryFlag = 1ul << 32; + private const ulong _jobFlag = 1ul << 33; + private ulong _data; + + public static FixedCondition TerritoryCondition(ushort territoryType) + => new() { _data = territoryType | _territoryFlag }; + + public static FixedCondition JobCondition(JobGroup group) + => new() { _data = group.Id | _jobFlag }; + + public bool Check(Actor actor) + { + if ((_data & (_territoryFlag | _jobFlag)) == 0) + return true; + + if ((_data & _territoryFlag) != 0) + return Dalamud.ClientState.TerritoryType == (ushort)_data; + + if (actor && GameData.JobGroups(Dalamud.GameData).TryGetValue((ushort)_data, out var group) && group.Fits(actor.Job)) + return true; + + return true; + } + + public override string ToString() + => _data.ToString(); +} diff --git a/Glamourer/Designs/FixedDesigns.cs b/Glamourer/Fixed/FixedDesigns.cs similarity index 83% rename from Glamourer/Designs/FixedDesigns.cs rename to Glamourer/Fixed/FixedDesigns.cs index fb8e15a..9b38cab 100644 --- a/Glamourer/Designs/FixedDesigns.cs +++ b/Glamourer/Fixed/FixedDesigns.cs @@ -2,69 +2,34 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Reflection.Metadata.Ecma335; using Dalamud.Logging; -using System.Runtime; using System.Text; using Dalamud.Utility; -using Glamourer.Interop; -using Glamourer.Structs; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Penumbra.GameData.Structs; -using Glamourer.Saves; using Penumbra.GameData.Actors; +using Glamourer.Designs; -namespace Glamourer.Designs; - -public struct FixedCondition -{ - private const ulong _territoryFlag = 1ul << 32; - private const ulong _jobFlag = 1ul << 33; - private ulong _data; - - public static FixedCondition TerritoryCondition(ushort territoryType) - => new() { _data = territoryType | _territoryFlag }; - - public static FixedCondition JobCondition(JobGroup group) - => new() { _data = group.Id | _jobFlag }; - - public bool Check(Actor actor) - { - if ((_data & (_territoryFlag | _jobFlag)) == 0) - return true; - - if ((_data & _territoryFlag) != 0) - return Dalamud.ClientState.TerritoryType == (ushort)_data; - - if (actor && GameData.JobGroups(Dalamud.GameData).TryGetValue((ushort)_data, out var group) && group.Fits(actor.Job)) - return true; - - return true; - } - - public override string ToString() - => _data.ToString(); -} +namespace Glamourer.Fixed; public class FixedDesign { public const int CurrentVersion = 0; - public string Name { get; private set; } - public bool Enabled; - public List Actors; + public string Name { get; private set; } + public bool Enabled; + public List Actors; public List<(FixedCondition, Design)> Customization; public List<(FixedCondition, Design)> Equipment; public List<(FixedCondition, Design)> Weapons; public FixedDesign(string name) { - Name = name; - Actors = new List(); + Name = name; + Actors = new List(); Customization = new List<(FixedCondition, Design)>(); - Equipment = new List<(FixedCondition, Design)>(); - Weapons = new List<(FixedCondition, Design)>(); + Equipment = new List<(FixedCondition, Design)>(); + Weapons = new List<(FixedCondition, Design)>(); } public static FixedDesign? Load(JObject j) @@ -82,7 +47,7 @@ public class FixedDesign return version switch { CurrentVersion => LoadCurrentVersion(j, name), - _ => null, + _ => null, }; } catch (Exception e) @@ -100,7 +65,7 @@ public class FixedDesign Enabled = enabled, }; - var actors = j[nameof(Actors)]; + var actors = j[nameof(Actors)]; //foreach(var pair in actors?.Children().) return null; } @@ -164,7 +129,7 @@ public class FixedDesign public static bool Load(FileInfo path, [NotNullWhen(true)] out FixedDesign? result) { - result = null; + result = null!; return true; } } diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index f415c72..f6eed9d 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -1,16 +1,23 @@ -using System.Reflection; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Glamourer.Api; using Glamourer.Customization; +using Glamourer.Designs; using Glamourer.Gui; using Glamourer.Interop; using Glamourer.State; +using Glamourer.Util; +using ImGuizmoNET; +using OtterGui.Classes; using OtterGui.Log; -using Penumbra.GameData; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; +using FixedDesigns = Glamourer.State.FixedDesigns; namespace Glamourer; @@ -29,20 +36,21 @@ public class Glamourer : IDalamudPlugin Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion ?? "Unknown"; - public static GlamourerConfig Config = null!; - public static Logger Log = null!; - - public static IObjectIdentifier Identifier = null!; - public static ActorManager Actors = null!; - public static PenumbraAttach Penumbra = null!; - public static ICustomizationManager Customization = null!; - public static RestrictedGear RestrictedGear = null!; - public static RedrawManager RedrawManager = null!; + public static GlamourerConfig Config = null!; + public static Logger Log = null!; + public static ActorManager Actors = null!; + public static PenumbraAttach Penumbra = null!; + public static ICustomizationManager Customization = null!; + public static RedrawManager RedrawManager = null!; + public static ItemManager Items = null!; public readonly FixedDesigns FixedDesigns; public readonly CurrentManipulations CurrentManipulations; - private readonly WindowSystem _windowSystem = new("Glamourer"); - private readonly Interface _interface; + private readonly Design.Manager _designManager; + private readonly DesignFileSystem _fileSystem; + private readonly FrameworkManager _framework; + private readonly WindowSystem _windowSystem = new("Glamourer"); + private readonly Interface _interface; //public readonly DesignManager Designs; @@ -56,17 +64,22 @@ public class Glamourer : IDalamudPlugin Dalamud.Initialize(pluginInterface); Log = new Logger(); - Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData); + _framework = new FrameworkManager(Dalamud.Framework, Log); - Config = GlamourerConfig.Load(); + Items = new ItemManager(Dalamud.PluginInterface, Dalamud.GameData); + Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData); - Identifier = global::Penumbra.GameData.GameData.GetIdentifier(Dalamud.PluginInterface, Dalamud.GameData); - Penumbra = new PenumbraAttach(Config.AttachToPenumbra); + Backup.CreateBackup(pluginInterface.ConfigDirectory, BackupFiles(Dalamud.PluginInterface)); + Config = GlamourerConfig.Load(); + + Penumbra = new PenumbraAttach(Config.AttachToPenumbra); Actors = new ActorManager(Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, Dalamud.GameGui, i => (short)Penumbra.CutsceneParent(i)); + + _designManager = new Design.Manager(Dalamud.PluginInterface, _framework); + _fileSystem = new DesignFileSystem(_designManager, Dalamud.PluginInterface, _framework); FixedDesigns = new FixedDesigns(); CurrentManipulations = new CurrentManipulations(); - //Designs = new DesignManager(); //GlamourerIpc = new GlamourerIpc(Dalamud.ClientState, Dalamud.Objects, Dalamud.PluginInterface); RedrawManager = new RedrawManager(FixedDesigns, CurrentManipulations); @@ -80,7 +93,7 @@ public class Glamourer : IDalamudPlugin HelpMessage = $"Use Glamourer Functions: {HelpString}", }); - _interface = new Interface(this); + _interface = new Interface(CurrentManipulations, _designManager, _fileSystem); _windowSystem.AddWindow(_interface); Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; //FixedDesignManager.Flag((Human*)((Actor)Dalamud.ClientState.LocalPlayer?.Address).Pointer->GameObject.DrawObject, 0, &x); @@ -95,13 +108,15 @@ public class Glamourer : IDalamudPlugin public void Dispose() { - RedrawManager?.Dispose(); Penumbra?.Dispose(); if (_windowSystem != null) Dalamud.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _interface?.Dispose(); + _fileSystem?.Dispose(); //GlamourerIpc.Dispose(); + _framework?.Dispose(); + Items?.Dispose(); Dalamud.Commands.RemoveHandler(ApplyCommandString); Dalamud.Commands.RemoveHandler(MainCommandString); } @@ -241,4 +256,25 @@ public class Glamourer : IDalamudPlugin // return; //} } + + // Collect all relevant files for glamourer configuration. + private static IReadOnlyList BackupFiles(DalamudPluginInterface pi) + { + var list = new List(16) + { + pi.ConfigFile, + new(DesignFileSystem.GetDesignFileSystemFile(pi)), + }; + + var configDir = Dalamud.PluginInterface.ConfigDirectory; + if (Directory.Exists(configDir.FullName)) + { + list.Add(new FileInfo(Path.Combine(configDir.FullName, "Designs.json"))); // migration + var designDir = new DirectoryInfo(Path.Combine(configDir.FullName, Design.Manager.DesignFolderName)); + if (designDir.Exists) + list.AddRange(designDir.EnumerateFiles("*.json", SearchOption.TopDirectoryOnly)); + } + + return list; + } } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs index 461e02b..9714463 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -1,6 +1,4 @@ using System; -using System.Linq; -using System.Security.AccessControl; using Glamourer.Customization; using ImGuiNET; using OtterGui; diff --git a/Glamourer/Gui/Designs/DesignFileSystemSelector.cs b/Glamourer/Gui/Designs/DesignFileSystemSelector.cs new file mode 100644 index 0000000..a3d4789 --- /dev/null +++ b/Glamourer/Gui/Designs/DesignFileSystemSelector.cs @@ -0,0 +1,39 @@ +using Glamourer.Designs; +using OtterGui.FileSystem.Selector; + +namespace Glamourer.Gui.Designs; + +public sealed class DesignFileSystemSelector : FileSystemSelector +{ + private readonly Design.Manager _manager; + + public struct DesignState + { } + + public DesignFileSystemSelector(Design.Manager manager, DesignFileSystem fileSystem) + : base(fileSystem, Dalamud.KeyState) + { + _manager = manager; + _manager.DesignChange += OnDesignChange; + } + + public override void Dispose() + { + base.Dispose(); + _manager.DesignChange -= OnDesignChange; + } + + private void OnDesignChange(Design.Manager.DesignChangeType type, Design design, object? oldData) + { + switch (type) + { + case Design.Manager.DesignChangeType.ReloadedAll: + case Design.Manager.DesignChangeType.Renamed: + case Design.Manager.DesignChangeType.AddedTag: + case Design.Manager.DesignChangeType.ChangedTag: + case Design.Manager.DesignChangeType.RemovedTag: + SetFilterDirty(); + break; + } + } +} diff --git a/Glamourer/Gui/Designs/InterfaceDesigns.cs b/Glamourer/Gui/Designs/InterfaceDesigns.cs index 8621ff7..090fe0c 100644 --- a/Glamourer/Gui/Designs/InterfaceDesigns.cs +++ b/Glamourer/Gui/Designs/InterfaceDesigns.cs @@ -1,6 +1,9 @@ -namespace Glamourer.Gui.Designs; - +using Dalamud.Game.ClientState.Keys; +using Glamourer.Designs; +using OtterGui.Filesystem; +using OtterGui.FileSystem.Selector; +namespace Glamourer.Gui.Designs; //internal partial class Interface //{ diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.Items.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.Items.cs index f65fa16..8bdc14f 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.Items.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.Items.cs @@ -2,17 +2,16 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Interface; +using Glamourer.Structs; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Lumina.Text; using OtterGui; using OtterGui.Classes; -using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Item = Glamourer.Structs.Item; namespace Glamourer.Gui.Equipment; @@ -20,15 +19,15 @@ public partial class EquipmentDrawer { public const int ItemComboWidth = 320; - private sealed class ItemCombo : FilterComboBase + private sealed class ItemCombo : FilterComboBase { public readonly string Label; public readonly EquipSlot Slot; - public Lumina.Excel.GeneratedSheets.Item? LastItem; - private CharacterArmor _lastArmor; - private string _lastPreview = string.Empty; - private int _lastIndex; + public Item? LastItem; + private CharacterArmor _lastArmor; + private string _lastPreview = string.Empty; + private int _lastIndex; public ItemCombo(EquipSlot slot) : base(GetItems(slot), false) @@ -37,7 +36,7 @@ public partial class EquipmentDrawer Slot = slot; } - protected override string ToString(Item obj) + protected override string ToString(Item2 obj) => obj.Name; private static string GetLabel(EquipSlot slot) @@ -78,21 +77,21 @@ public partial class EquipmentDrawer _lastPreview = _lastIndex >= 0 ? Items[_lastIndex].Name : LastItem.Name.ToString(); } - private static IReadOnlyList GetItems(EquipSlot slot) - => GameData.ItemsBySlot(Dalamud.GameData).TryGetValue(slot, out var list) ? list : Array.Empty(); + private static IReadOnlyList GetItems(EquipSlot slot) + => GameData.ItemsBySlot(Dalamud.GameData).TryGetValue(slot, out var list) ? list : Array.Empty(); } - private sealed class WeaponCombo : FilterComboBase + private sealed class WeaponCombo : FilterComboBase { public readonly string Label; public readonly EquipSlot Slot; - public Lumina.Excel.GeneratedSheets.Item? LastItem; - private CharacterWeapon _lastWeapon = new(ulong.MaxValue); - private string _lastPreview = string.Empty; - private int _lastIndex; - public FullEquipType LastCategory { get; private set; } - private bool _drawAll; + public Item? LastItem; + private CharacterWeapon _lastWeapon = new(ulong.MaxValue); + private string _lastPreview = string.Empty; + private int _lastIndex; + public FullEquipType LastCategory { get; private set; } + private bool _drawAll; public WeaponCombo(EquipSlot slot) : base(GetItems(slot), false) @@ -101,7 +100,7 @@ public partial class EquipmentDrawer Slot = slot; } - protected override string ToString(Item obj) + protected override string ToString(Item2 obj) => obj.Name; private static string GetLabel(EquipSlot slot) @@ -124,7 +123,7 @@ public partial class EquipmentDrawer } UpdateItem(weapon); - UpdateCategory(((WeaponCategory) (LastItem!.ItemUICategory?.Row ?? 0)).ToEquipType()); + UpdateCategory(((WeaponCategory)(LastItem!.ItemUICategory?.Row ?? 0)).ToEquipType()); newIdx = _lastIndex; return Draw(Label, _lastPreview, ref newIdx, ItemComboWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeight()); } @@ -175,8 +174,8 @@ public partial class EquipmentDrawer ResetFilter(); } - private static IReadOnlyList GetItems(EquipSlot slot) - => GameData.ItemsBySlot(Dalamud.GameData).TryGetValue(EquipSlot.MainHand, out var list) ? list : Array.Empty(); + private static IReadOnlyList GetItems(EquipSlot slot) + => GameData.ItemsBySlot(Dalamud.GameData).TryGetValue(EquipSlot.MainHand, out var list) ? list : Array.Empty(); } private static readonly IObjectIdentifier Identifier; @@ -203,15 +202,15 @@ public partial class EquipmentDrawer UpdateActors(); } - private static CharacterArmor ToArmor(Item item, StainId stain) + private static CharacterArmor ToArmor(Item2 item2, StainId stain) { - var (id, _, variant) = item.MainModel; + var (id, _, variant) = item2.MainModel; return new CharacterArmor(id, (byte)variant, stain); } - private static CharacterWeapon ToWeapon(Item item, StainId stain) + private static CharacterWeapon ToWeapon(Item2 item2, StainId stain) { - var (id, type, variant) = item.MainModel; + var (id, type, variant) = item2.MainModel; return new CharacterWeapon(id, type, variant, stain); } @@ -331,31 +330,31 @@ public partial class EquipmentDrawer // - private static readonly Lumina.Excel.GeneratedSheets.Item SmallClothes = new() + private static readonly Item SmallClothes = new() { Name = new SeString("Nothing"), RowId = 0, }; - private static readonly Lumina.Excel.GeneratedSheets.Item SmallClothesNpc = new() + private static readonly Item SmallClothesNpc = new() { Name = new SeString("Smallclothes (NPC)"), RowId = 1, }; - private static readonly Lumina.Excel.GeneratedSheets.Item Unknown = new() + private static readonly Item Unknown = new() { Name = new SeString("Unknown"), RowId = 2, }; - private static Lumina.Excel.GeneratedSheets.Item Identify(SetId set, WeaponType weapon, ushort variant, EquipSlot slot) + private static Item Identify(SetId set, WeaponType weapon, ushort variant, EquipSlot slot) { return (uint)set switch { 0 => SmallClothes, 9903 => SmallClothesNpc, - _ => Identifier.Identify(set, weapon, variant, slot).FirstOrDefault(Unknown), + _ => Identifier.Identify(set, weapon, variant, slot.ToSlot()).FirstOrDefault(Unknown), }; } } diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.Main.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.Main.cs index e2c824d..9d42a78 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.Main.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.Main.cs @@ -17,8 +17,8 @@ public enum ApplicationFlags public partial class EquipmentDrawer { - private static readonly FilterComboColors _stainCombo; - private static readonly StainData _stainData; + private static readonly FilterComboColors StainCombo; + private static readonly StainData StainData; private Race _race; private Gender _gender; @@ -31,10 +31,10 @@ public partial class EquipmentDrawer static EquipmentDrawer() { - _stainData = new StainData(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.GameData.Language); - _stainCombo = new FilterComboColors(140, - _stainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false)))); - Identifier = Glamourer.Identifier; + StainData = Glamourer.Items.Stains; + StainCombo = new FilterComboColors(140, + StainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false)))); + Identifier = Glamourer.Items.Identifier; ItemCombos = EquipSlotExtensions.EqdpSlots.Select(s => new ItemCombo(s)).ToArray(); MainHandCombo = new WeaponCombo(EquipSlot.MainHand); OffHandCombo = new WeaponCombo(EquipSlot.OffHand); @@ -82,8 +82,8 @@ public partial class EquipmentDrawer private void DrawStainCombo() { - var found = _stainData.TryGetValue(_currentArmor.Stain, out var stain); - _stainCombo.Draw("##stain", stain.RgbaColor, found); + var found = StainData.TryGetValue(_currentArmor.Stain, out var stain); + StainCombo.Draw("##stain", stain.RgbaColor, found); } private void DrawInternal(ref CharacterWeapon mainHand, ref CharacterWeapon offHand) diff --git a/Glamourer/Gui/Interface.Actors.cs b/Glamourer/Gui/Interface.Actors.cs index 3b00eeb..70f27ed 100644 --- a/Glamourer/Gui/Interface.Actors.cs +++ b/Glamourer/Gui/Interface.Actors.cs @@ -1,9 +1,6 @@ using System; -using System.Diagnostics; -using System.Linq; using System.Numerics; using Dalamud.Interface; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Interop; @@ -13,7 +10,6 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; using ImGui = ImGuiNET.ImGui; namespace Glamourer.Gui; @@ -30,7 +26,7 @@ internal partial class Interface private ActorIdentifier _identifier = ActorIdentifier.Invalid; private ObjectManager.ActorData _currentData = ObjectManager.ActorData.Invalid; private string _currentLabel = string.Empty; - private CurrentDesign? _currentSave; + private ActiveDesign? _currentSave; public void Draw() { diff --git a/Glamourer/Gui/Interface.DebugStateTab.cs b/Glamourer/Gui/Interface.DebugStateTab.cs index 19eae46..716a0dd 100644 --- a/Glamourer/Gui/Interface.DebugStateTab.cs +++ b/Glamourer/Gui/Interface.DebugStateTab.cs @@ -21,7 +21,7 @@ internal partial class Interface private LowerString _manipulationFilter = LowerString.Empty; private ActorIdentifier _selection = ActorIdentifier.Invalid; - private CurrentDesign? _save = null; + private ActiveDesign? _save = null; private bool _delete = false; public DebugStateTab(CurrentManipulations currentManipulations) @@ -73,14 +73,14 @@ internal partial class Interface DrawSelector(oldSpacing); } - private bool CheckFilter(KeyValuePair data) + private bool CheckFilter(KeyValuePair data) { if (data.Key.Equals(_selection)) _save = data.Value; return _manipulationFilter.Length == 0 || _manipulationFilter.IsContained(data.Key.ToString()!); } - private void DrawSelectable(KeyValuePair data) + private void DrawSelectable(KeyValuePair data) { var equal = data.Key.Equals(_selection); if (ImGui.Selectable(data.Key.ToString(), equal)) diff --git a/Glamourer/Gui/Interface.DesignTab.cs b/Glamourer/Gui/Interface.DesignTab.cs new file mode 100644 index 0000000..a0441e9 --- /dev/null +++ b/Glamourer/Gui/Interface.DesignTab.cs @@ -0,0 +1,64 @@ +using System; +using System.Numerics; +using Dalamud.Interface; +using Glamourer.Designs; +using Glamourer.Gui.Customization; +using Glamourer.Gui.Designs; +using Glamourer.Gui.Equipment; +using Glamourer.Interop; +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui; + +internal partial class Interface +{ + private class DesignTab : IDisposable + { + public readonly DesignFileSystemSelector Selector; + private readonly DesignFileSystem _fileSystem; + private readonly Design.Manager _manager; + + public DesignTab(Design.Manager manager, DesignFileSystem fileSystem) + { + _manager = manager; + _fileSystem = fileSystem; + Selector = new DesignFileSystemSelector(manager, fileSystem); + } + + public void Dispose() + => Selector.Dispose(); + + public void Draw() + { + using var tab = ImRaii.TabItem("Designs"); + if (!tab) + { + return; + } + + Selector.Draw(GetDesignSelectorSize()); + ImGui.SameLine(); + DrawDesignPanel(); + } + + public float GetDesignSelectorSize() + => 200f * ImGuiHelpers.GlobalScale; + + public void DrawDesignPanel() + { + using var child = ImRaii.Child("##DesignPanel", new Vector2(-0.001f), true, ImGuiWindowFlags.HorizontalScrollbar); + if (!child || Selector.Selected == null) + return; + + CustomizationDrawer.Draw(Selector.Selected.Customize(), Selector.Selected.Equipment(), true); + var weapon = Selector.Selected.WeaponMain; + var mw = new CharacterWeapon(weapon.ModelBase, weapon.WeaponBase, weapon.Variant, weapon.Stain); + weapon = Selector.Selected.WeaponOff; + var ow = new CharacterWeapon(weapon.ModelBase, weapon.WeaponBase, weapon.Variant, weapon.Stain); + ApplicationFlags f = 0; + EquipmentDrawer.Draw(Selector.Selected.Customize(), Selector.Selected.Equipment(), ref mw, ref ow, ref f, Array.Empty(), true); + } + } +} diff --git a/Glamourer/Gui/Interface.SettingsTab.cs b/Glamourer/Gui/Interface.SettingsTab.cs index 1cf6be8..9bbd065 100644 --- a/Glamourer/Gui/Interface.SettingsTab.cs +++ b/Glamourer/Gui/Interface.SettingsTab.cs @@ -1,5 +1,4 @@ using System; -using System.Numerics; using Glamourer.State; using ImGuiNET; using OtterGui; diff --git a/Glamourer/Gui/Interface.cs b/Glamourer/Gui/Interface.cs index 57ee909..4a638a3 100644 --- a/Glamourer/Gui/Interface.cs +++ b/Glamourer/Gui/Interface.cs @@ -3,7 +3,9 @@ using System.Linq; using System.Numerics; using Dalamud.Interface.Windowing; using Dalamud.Logging; +using Glamourer.Designs; using Glamourer.Gui.Customization; +using Glamourer.State; using ImGuiNET; using OtterGui.Raii; @@ -11,16 +13,14 @@ namespace Glamourer.Gui; internal partial class Interface : Window, IDisposable { - private readonly Glamourer _plugin; - private readonly ActorTab _actorTab; + private readonly DesignTab _designTab; private readonly DebugStateTab _debugStateTab; private readonly DebugDataTab _debugDataTab; - public Interface(Glamourer plugin) + public Interface(CurrentManipulations manipulations, Design.Manager manager, DesignFileSystem fileSystem) : base(GetLabel()) { - _plugin = plugin; Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; Dalamud.PluginInterface.UiBuilder.OpenConfigUi += Toggle; SizeConstraints = new WindowSizeConstraints() @@ -28,9 +28,10 @@ internal partial class Interface : Window, IDisposable MinimumSize = new Vector2(675, 675), MaximumSize = ImGui.GetIO().DisplaySize, }; - _actorTab = new ActorTab(_plugin.CurrentManipulations); - _debugStateTab = new DebugStateTab(_plugin.CurrentManipulations); + _actorTab = new ActorTab(manipulations); + _debugStateTab = new DebugStateTab(manipulations); _debugDataTab = new DebugDataTab(Glamourer.Customization); + _designTab = new DesignTab(manager, fileSystem); } public override void Draw() @@ -44,6 +45,7 @@ internal partial class Interface : Window, IDisposable UpdateState(); _actorTab.Draw(); + _designTab.Draw(); DrawSettingsTab(); _debugStateTab.Draw(); _debugDataTab.Draw(); @@ -61,6 +63,7 @@ internal partial class Interface : Window, IDisposable { Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= Toggle; CustomizationDrawer.Dispose(); + _designTab.Dispose(); } private static string GetLabel() diff --git a/Glamourer/Gui/InterfaceInitialization.cs b/Glamourer/Gui/InterfaceInitialization.cs index ca2fa18..ea3fa1e 100644 --- a/Glamourer/Gui/InterfaceInitialization.cs +++ b/Glamourer/Gui/InterfaceInitialization.cs @@ -4,7 +4,6 @@ using System.Reflection; using ImGuiNET; using Penumbra.GameData.Enums; using Lumina.Excel.GeneratedSheets; -using Item = Glamourer.Structs.Item; namespace Glamourer.Gui; diff --git a/Glamourer/Interop/Actor.cs b/Glamourer/Interop/Actor.cs index bf781b1..5276f89 100644 --- a/Glamourer/Interop/Actor.cs +++ b/Glamourer/Interop/Actor.cs @@ -1,7 +1,6 @@ using System; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; -using FFXIVClientStructs.FFXIV.Client.System.String; using Glamourer.Customization; using Penumbra.GameData.Actors; using Penumbra.GameData.Structs; diff --git a/Glamourer/Interop/Offsets.cs b/Glamourer/Interop/Offsets.cs deleted file mode 100644 index 7127e8e..0000000 --- a/Glamourer/Interop/Offsets.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Glamourer.Interop; - -public static class Offsets -{ - public static class Character - { - public const int Wetness = 0x1ADA; - public const int HatVisible = 0x84E; - public const int VisorToggled = 0x84F; - public const int WeaponHidden1 = 0x84F; - public const int WeaponHidden2 = 0x72C; - public const int Alpha = 0x19E0; - - public static class Flags - { - public const byte IsHatHidden = 0x01; - public const byte IsVisorToggled = 0x08; - public const byte IsWet = 0x80; - public const byte IsWeaponHidden1 = 0x01; - public const byte IsWeaponHidden2 = 0x02; - } - } -} diff --git a/Glamourer/Interop/RedrawManager.Customize.cs b/Glamourer/Interop/RedrawManager.Customize.cs index 1405b69..4451078 100644 --- a/Glamourer/Interop/RedrawManager.Customize.cs +++ b/Glamourer/Interop/RedrawManager.Customize.cs @@ -14,6 +14,8 @@ public unsafe partial class RedrawManager [Signature("E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86")] private readonly ChangeCustomizeDelegate _changeCustomize = null!; + + public bool UpdateCustomize(Actor actor, Customize customize) { if (!actor.Valid || !actor.DrawObject.Valid) @@ -43,10 +45,11 @@ public unsafe partial class RedrawManager return; var flags = &data->CharacterBase.UnkFlags_01; - var state = (*flags & 0x40) != 0; + var state = (*flags & Offsets.DrawObjectVisorStateFlag) != 0; if (state == on) return; - *flags = (byte)((on ? *flags | 0x40 : *flags & 0xBF) | 0x80); + var newFlag = (byte)(on ? *flags | Offsets.DrawObjectVisorStateFlag : *flags & ~Offsets.DrawObjectVisorStateFlag); + *flags = (byte) (newFlag | Offsets.DrawObjectVisorToggleFlag); } } diff --git a/Glamourer/Interop/RedrawManager.Equipment.cs b/Glamourer/Interop/RedrawManager.Equipment.cs index 2e7619d..586a371 100644 --- a/Glamourer/Interop/RedrawManager.Equipment.cs +++ b/Glamourer/Interop/RedrawManager.Equipment.cs @@ -1,8 +1,7 @@ using System; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Designs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -10,59 +9,76 @@ namespace Glamourer.Interop; public unsafe partial class RedrawManager { - public delegate ulong FlagSlotForUpdateDelegate(Human* drawObject, uint slot, CharacterArmor* data); + private delegate ulong FlagSlotForUpdateDelegate(nint drawObject, uint slot, CharacterArmor* data); // This gets called when one of the ten equip items of an existing draw object gets changed. - [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A", DetourName = nameof(FlagSlotForUpdateDetour))] + [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))] private readonly Hook _flagSlotForUpdateHook = null!; - private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slotIdx, CharacterArmor* data) + public void UpdateSlot(DrawObject drawObject, EquipSlot slot, CharacterArmor data) + => FlagSlotForUpdateDetourBase(drawObject.Address, slot.ToIndex(), &data, true); + + public void UpdateStain(DrawObject drawObject, EquipSlot slot, StainId stain) + { + var armor = drawObject.Equip[slot] with { Stain = stain}; + UpdateSlot(drawObject, slot, armor); + } + + private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) + => FlagSlotForUpdateDetourBase(drawObject, slotIdx, data, false); + + private ulong FlagSlotForUpdateDetourBase(nint drawObject, uint slotIdx, CharacterArmor* data, bool manual) { - var slot = slotIdx.ToEquipSlot(); try { - var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject); - var identifier = actor.GetIdentifier(); - - if (_fixedDesigns.TryGetDesign(identifier, out var save)) - { - PluginLog.Information($"Loaded {slot} from fixed design for {identifier}."); - (var replaced, *data) = - Glamourer.RestrictedGear.ResolveRestricted(save.Equipment[slot], slot, (Race)drawObject->Race, (Gender)drawObject->Sex); - } - else if (_currentManipulations.TryGetDesign(identifier, out var save2)) - { - PluginLog.Information($"Updated {slot} from current designs for {identifier}."); - (var replaced, *data) = - Glamourer.RestrictedGear.ResolveRestricted(*data, slot, (Race)drawObject->Race, (Gender)(drawObject->Sex + 1)); - save2.Data.Equipment[slot] = *data; - } + var slot = slotIdx.ToEquipSlot(); + Glamourer.Log.Verbose( + $"Flagged slot {slot} of 0x{(ulong)drawObject:X} for update with {data->Set.Value}-{data->Variant} (Stain {data->Stain.Value})."); + HandleEquipUpdate(drawObject, slot, ref *data, manual); } - catch (Exception e) + catch (Exception ex) { - PluginLog.Error($"Error on loading new gear:\n{e}"); + Glamourer.Log.Error($"Error invoking SlotUpdate:\n{ex}"); } return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data); + + //try + //{ + // var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject); + // var identifier = actor.GetIdentifier(); + // + // if (_fixedDesigns.TryGetDesign(identifier, out var design)) + // { + // PluginLog.Information($"Loaded {slot} from fixed design for {identifier}."); + // (var replaced, *data) = + // Glamourer.Items.RestrictedGear.ResolveRestricted(design.Armor(slot).Model, slot, (Race)drawObject->Race, (Gender)drawObject->Sex); + // } + // else if (_currentManipulations.TryGetDesign(identifier, out var save2)) + // { + // PluginLog.Information($"Updated {slot} from current designs for {identifier}."); + // (var replaced, *data) = + // Glamourer.Items.RestrictedGear.ResolveRestricted(*data, slot, (Race)drawObject->Race, (Gender)(drawObject->Sex + 1)); + // save2.Data.Equipment[slot] = *data; + // } + //} + //catch (Exception e) + //{ + // PluginLog.Error($"Error on loading new gear:\n{e}"); + //} + // + //return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data); } - public bool ChangeEquip(DrawObject drawObject, uint slotIdx, CharacterArmor data) + private void HandleEquipUpdate(nint drawObject, EquipSlot slot, ref CharacterArmor data, bool manual) { - if (!drawObject) - return false; + var actor = Glamourer.Penumbra.GameObjectFromDrawObject(drawObject); + var identifier = actor.GetIdentifier(); - if (slotIdx > 9) - return false; + if (!_currentManipulations.TryGetDesign(identifier, out var design)) + return; - return FlagSlotForUpdateDetour(drawObject.Pointer, slotIdx, &data) != 0; + var flag = slot.ToFlag(); + var stainFlag = slot.ToStainFlag(); } - - public bool ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data) - => actor && ChangeEquip(actor.DrawObject, slot.ToIndex(), data); - - public bool ChangeEquip(DrawObject drawObject, EquipSlot slot, CharacterArmor data) - => ChangeEquip(drawObject, slot.ToIndex(), data); - - public bool ChangeEquip(Actor actor, uint slotIdx, CharacterArmor data) - => actor && ChangeEquip(actor.DrawObject, slotIdx, data); } diff --git a/Glamourer/Interop/RedrawManager.Weapons.cs b/Glamourer/Interop/RedrawManager.Weapons.cs index 00809bf..6c0bda7 100644 --- a/Glamourer/Interop/RedrawManager.Weapons.cs +++ b/Glamourer/Interop/RedrawManager.Weapons.cs @@ -40,8 +40,8 @@ public unsafe partial class RedrawManager PluginLog.Information($"Loaded weapon from fixed design for {identifier}."); weapon = slot switch { - 0 => save.MainHand.Value, - 1 => save.OffHand.Value, + 0 => save.WeaponMain.Model.Value, + 1 => save.WeaponOff.Model.Value, _ => weapon, }; } diff --git a/Glamourer/Interop/RedrawManager.cs b/Glamourer/Interop/RedrawManager.cs index 6815d08..37d5e21 100644 --- a/Glamourer/Interop/RedrawManager.cs +++ b/Glamourer/Interop/RedrawManager.cs @@ -2,27 +2,40 @@ using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Customization; using Glamourer.State; using Glamourer.Structs; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using CustomizeData = Penumbra.GameData.Structs.CustomizeData; namespace Glamourer.Interop; +public class DesignBaseValidator +{ + private readonly CustomizationManager _manager; + private readonly RestrictedGear _restrictedGear; + + public DesignBaseValidator(CustomizationManager manager, RestrictedGear restrictedGear) + { + _manager = manager; + _restrictedGear = restrictedGear; + } +} + + public unsafe partial class RedrawManager { private delegate void ChangeJobDelegate(IntPtr data, uint job); - [Signature("88 51 ?? 44 3B CA", DetourName = nameof(ChangeJobDetour))] + [Signature(Sigs.ChangeJob, DetourName = nameof(ChangeJobDetour))] private readonly Hook _changeJobHook = null!; private void ChangeJobDetour(IntPtr data, uint job) { _changeJobHook.Original(data, job); - JobChanged?.Invoke(data - 0x1A8, GameData.Jobs(Dalamud.GameData)[(byte)job]); + JobChanged?.Invoke(data - Offsets.Character.ClassJobContainer, GameData.Jobs(Dalamud.GameData)[(byte)job]); } public event Action? JobChanged; @@ -70,18 +83,18 @@ public unsafe partial class RedrawManager : IDisposable // Apply customization if they correspond and there is customization to apply. var gameObjectCustomize = new Customize((CustomizeData*)actor.Pointer->CustomizeData); if (gameObjectCustomize.Equals(customize)) - customize.Load(save!.Data.Customize); + customize.Load(save!.Customize()); // Compare game object equip data against draw object equip data for transformations. // Apply each piece of equip that should be applied if they correspond. var gameObjectEquip = new CharacterEquip((CharacterArmor*)actor.Pointer->EquipSlotData); if (gameObjectEquip.Equals(equip)) { - var saveEquip = save!.Data.Equipment; + var saveEquip = save!.Equipment(); foreach (var slot in EquipSlotExtensions.EqdpSlots) { (_, equip[slot]) = - Glamourer.RestrictedGear.ResolveRestricted(saveEquip[slot], slot, customize.Race, customize.Gender); + Glamourer.Items.RestrictedGear.ResolveRestricted(saveEquip[slot], slot, customize.Race, customize.Gender); } } } diff --git a/Glamourer/Saves/Design.cs b/Glamourer/Saves/Design.cs deleted file mode 100644 index 4a5ec93..0000000 --- a/Glamourer/Saves/Design.cs +++ /dev/null @@ -1,305 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Glamourer.Customization; -using Glamourer.Interop; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Saves; - -public partial class Design -{ - public const int CurrentVersion = 1; - - public FileInfo Identifier { get; private set; } = new(string.Empty); - public string Name { get; private set; } = "New Design"; - public string Description { get; private set; } = string.Empty; - - public DateTimeOffset CreationDate { get; private init; } = DateTimeOffset.UtcNow; - public DateTimeOffset LastUpdateDate { get; private set; } = DateTimeOffset.UtcNow; - - private DesignFlagsV1 _flags; - - public bool VisorState - { - get => _flags.HasFlag(DesignFlagsV1.VisorState); - private set => _flags = value ? _flags | DesignFlagsV1.VisorState : _flags & ~DesignFlagsV1.VisorState; - } - - public bool VisorApply - { - get => _flags.HasFlag(DesignFlagsV1.VisorApply); - private set => _flags = value ? _flags | DesignFlagsV1.VisorApply : _flags & ~DesignFlagsV1.VisorApply; - } - - public bool WeaponStateShown - { - get => _flags.HasFlag(DesignFlagsV1.WeaponStateShown); - private set => _flags = value ? _flags | DesignFlagsV1.WeaponStateShown : _flags & ~DesignFlagsV1.WeaponStateShown; - } - - public bool WeaponStateApply - { - get => _flags.HasFlag(DesignFlagsV1.WeaponStateApply); - private set => _flags = value ? _flags | DesignFlagsV1.WeaponStateApply : _flags & ~DesignFlagsV1.WeaponStateApply; - } - - public bool WetnessState - { - get => _flags.HasFlag(DesignFlagsV1.WetnessState); - private set => _flags = value ? _flags | DesignFlagsV1.WetnessState : _flags & ~DesignFlagsV1.WetnessState; - } - - public bool WetnessApply - { - get => _flags.HasFlag(DesignFlagsV1.WetnessApply); - private set => _flags = value ? _flags | DesignFlagsV1.WetnessApply : _flags & ~DesignFlagsV1.WetnessApply; - } - - public bool ReadOnly - { - get => _flags.HasFlag(DesignFlagsV1.ReadOnly); - private set => _flags = value ? _flags | DesignFlagsV1.ReadOnly : _flags & ~DesignFlagsV1.ReadOnly; - } - - private static bool FromDesignable(string identifier, string name, IDesignable data, [NotNullWhen(true)] out Design? design, - bool doWeapons = true, bool doFlags = true, bool doEquipment = true, bool doCustomize = true) - { - if (!data.Valid) - { - design = null; - return false; - } - - design = new Design - { - Identifier = new FileInfo(identifier), - Name = name, - Description = string.Empty, - CreationDate = DateTimeOffset.UtcNow, - LastUpdateDate = DateTimeOffset.UtcNow, - ReadOnly = false, - VisorApply = doFlags, - WeaponStateApply = doFlags, - WetnessApply = doFlags, - VisorState = data.VisorEnabled, - WeaponStateShown = data.WeaponEnabled, - WetnessState = data.IsWet, - }; - - if (doEquipment) - { - var equipment = data.Equip; - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - var s = design[slot]; - var e = equipment[slot]; - s.StainId = e.Stain; - s.ApplyStain = true; - s.ItemId = Glamourer.Identifier.Identify(e.Set, e.Variant, slot).FirstOrDefault()?.RowId ?? 0; - s.ApplyItem = s.ItemId != 0; - } - } - - if (doWeapons) - { - var m = design.MainHand; - var d = data.MainHand; - - m.StainId = d.Stain; - m.ApplyStain = true; - m.ItemId = Glamourer.Identifier.Identify(d.Set, d.Type, d.Variant, EquipSlot.MainHand).FirstOrDefault()?.RowId ?? 0; - m.ApplyItem = m.ItemId != 0; - - var o = design.OffHand; - d = data.OffHand; - o.StainId = d.Stain; - o.ApplyStain = true; - o.ItemId = Glamourer.Identifier.Identify(d.Set, d.Type, d.Variant, EquipSlot.MainHand).FirstOrDefault()?.RowId ?? 0; - o.ApplyItem = o.ItemId != 0; - } - - if (doCustomize) - { - var customize = data.Customize; - design.CustomizeFlags = Glamourer.Customization.GetList(customize.Clan, customize.Gender).SettingAvailable - | CustomizeFlag.Gender - | CustomizeFlag.Race - | CustomizeFlag.Clan; - foreach (var c in Enum.GetValues()) - { - if (!design.CustomizeFlags.HasFlag(c.ToFlag())) - continue; - - var choice = design[c]; - choice.Value = customize[c]; - } - } - - - return true; - } - - public void Save() - { - try - { - using var file = File.Open(Identifier.FullName, File.Exists(Identifier.FullName) ? FileMode.Truncate : FileMode.CreateNew); - WriteJson(file); - } - catch (Exception ex) - { - Glamourer.Log.Error($"Could not save design {Identifier.Name}:\n{ex}"); - } - } - - public void WriteJson(Stream s, Formatting formatting = Formatting.Indented) - { - var obj = new JObject(); - obj["Version"] = CurrentVersion; - obj[nameof(Name)] = Name; - obj[nameof(Description)] = Description; - obj[nameof(CreationDate)] = CreationDate.ToUnixTimeSeconds(); - obj[nameof(LastUpdateDate)] = LastUpdateDate.ToUnixTimeSeconds(); - obj[nameof(ReadOnly)] = ReadOnly; - WriteEquipment(obj); - WriteCustomization(obj); - WriteFlags(obj); - - using var t = new StreamWriter(s); - using var j = new JsonTextWriter(t) { Formatting = formatting }; - obj.WriteTo(j); - } - - private void WriteFlags(JObject obj) - { - obj[nameof(VisorState)] = VisorState; - obj[nameof(VisorApply)] = VisorApply; - obj[nameof(WeaponStateShown)] = WeaponStateShown; - obj[nameof(WeaponStateApply)] = WeaponStateApply; - obj[nameof(WetnessState)] = WetnessState; - obj[nameof(WetnessApply)] = WetnessApply; - } - - public static bool Load(string fileName, [NotNullWhen(true)] out Design? design) - { - design = null; - if (!File.Exists(fileName)) - { - Glamourer.Log.Error($"Could not load design {fileName}:\nFile does not exist."); - return false; - } - - try - { - var data = File.ReadAllText(fileName); - var obj = JObject.Parse(data); - - return obj["Version"]?.Value() switch - { - null => NoVersion(fileName), - 1 => LoadV1(fileName, obj, out design), - _ => UnknownVersion(fileName, obj["Version"]!.Value()), - }; - } - catch (Exception e) - { - Glamourer.Log.Error($"Could not load design {fileName}:\n{e}"); - } - - return false; - } - - private static bool NoVersion(string fileName) - { - Glamourer.Log.Error($"Could not load design {fileName}:\nNo version available."); - return false; - } - - private static bool UnknownVersion(string fileName, int version) - { - Glamourer.Log.Error($"Could not load design {fileName}:\nThe version {version} can not be handled."); - return false; - } - - private static bool LoadV1(string fileName, JObject obj, [NotNullWhen(true)] out Design? design) - { - design = new Design - { - Identifier = new FileInfo(fileName), - Name = obj[nameof(Name)]?.Value() ?? "New Design", - Description = obj[nameof(Description)]?.Value() ?? string.Empty, - CreationDate = GetDateTime(obj[nameof(CreationDate)]?.Value()), - LastUpdateDate = GetDateTime(obj[nameof(LastUpdateDate)]?.Value()), - ReadOnly = obj[nameof(ReadOnly)]?.Value() ?? false, - VisorState = obj[nameof(VisorState)]?.Value() ?? false, - VisorApply = obj[nameof(VisorApply)]?.Value() ?? false, - WeaponStateShown = obj[nameof(WeaponStateShown)]?.Value() ?? false, - WeaponStateApply = obj[nameof(WeaponStateApply)]?.Value() ?? false, - WetnessState = obj[nameof(WetnessState)]?.Value() ?? false, - WetnessApply = obj[nameof(WetnessApply)]?.Value() ?? false, - }; - - var equipment = obj[nameof(Equipment)]; - if (equipment == null) - { - design.EquipmentFlags = 0; - design.StainFlags = 0; - design._equipmentData = default; - } - else - { - foreach (var slot in design.Equipment) - { - var s = equipment[SlotName[slot.Index]]; - if (s == null) - { - slot.ItemId = 0; - slot.ApplyItem = false; - slot.ApplyStain = false; - slot.StainId = 0; - } - else - { - slot.ItemId = s[nameof(Slot.ItemId)]?.Value() ?? 0u; - slot.ApplyItem = obj[nameof(Slot.ApplyItem)]?.Value() ?? false; - slot.StainId = new StainId(s[nameof(Slot.StainId)]?.Value() ?? 0); - slot.ApplyStain = obj[nameof(Slot.ApplyStain)]?.Value() ?? false; - } - } - } - - var customize = obj[nameof(Customization)]; - if (customize == null) - { - design.CustomizeFlags = 0; - design._customizeData = Customize.Default; - } - else - { - foreach (var choice in design.Customization) - { - var c = customize[choice.Index.ToDefaultName()]; - if (c == null) - { - choice.Value = Customize.Default.Get(choice.Index); - choice.Apply = false; - } - else - { - choice.Value = new CustomizeValue(c[nameof(Choice.Value)]?.Value() ?? Customize.Default.Get(choice.Index).Value); - choice.Apply = c[nameof(Choice.Apply)]?.Value() ?? false; - } - } - } - - return true; - } - - private static DateTimeOffset GetDateTime(long? value) - => value == null ? DateTimeOffset.UtcNow : DateTimeOffset.FromUnixTimeSeconds(value.Value); -} diff --git a/Glamourer/Saves/DesignFlagsV1.cs b/Glamourer/Saves/DesignFlagsV1.cs deleted file mode 100644 index 6163f3e..0000000 --- a/Glamourer/Saves/DesignFlagsV1.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Glamourer.Saves; - -[Flags] -public enum DesignFlagsV1 : byte -{ - VisorState = 0x01, - VisorApply = 0x02, - WeaponStateShown = 0x04, - WeaponStateApply = 0x08, - WetnessState = 0x10, - WetnessApply = 0x20, - ReadOnly = 0x40, -} diff --git a/Glamourer/Saves/EquipmentDesign.cs b/Glamourer/Saves/EquipmentDesign.cs deleted file mode 100644 index 3eb6e41..0000000 --- a/Glamourer/Saves/EquipmentDesign.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Saves; - -public partial class Design -{ - private unsafe struct EquipmentData - { - public const int NumEquipment = 12; - public fixed uint Ids[NumEquipment]; - public fixed byte Stains[NumEquipment]; - } - - private EquipmentData _equipmentData = default; - public ushort EquipmentFlags { get; private set; } - public ushort StainFlags { get; private set; } - - // @formatter:off - public Slot Head => new(this, 0); - public Slot Body => new(this, 1); - public Slot Hands => new(this, 2); - public Slot Legs => new(this, 3); - public Slot Feet => new(this, 4); - public Slot Ears => new(this, 5); - public Slot Neck => new(this, 6); - public Slot Wrists => new(this, 7); - public Slot RFinger => new(this, 8); - public Slot LFinger => new(this, 9); - public Slot MainHand => new(this, 10); - public Slot OffHand => new(this, 11); - // @formatter:on - - public Slot this[EquipSlot slot] - => new(this, (int)slot.ToIndex()); - - - public static readonly string[] SlotName = - { - EquipSlot.Head.ToName(), - EquipSlot.Body.ToName(), - EquipSlot.Hands.ToName(), - EquipSlot.Legs.ToName(), - EquipSlot.Feet.ToName(), - EquipSlot.Ears.ToName(), - EquipSlot.Neck.ToName(), - EquipSlot.Wrists.ToName(), - EquipSlot.RFinger.ToName(), - EquipSlot.LFinger.ToName(), - EquipSlot.MainHand.ToName(), - EquipSlot.OffHand.ToName(), - }; - - - public readonly unsafe struct Slot - { - private readonly Design _data; - public readonly int Index; - public readonly ushort Flag; - - public Slot(Design design, int idx) - { - _data = design; - Index = idx; - Flag = (ushort)(1 << idx); - } - - public uint ItemId - { - get => _data._equipmentData.Ids[Index]; - set => _data._equipmentData.Ids[Index] = value; - } - - public StainId StainId - { - get => _data._equipmentData.Stains[Index]; - set => _data._equipmentData.Stains[Index] = value.Value; - } - - public bool ApplyItem - { - get => (_data.EquipmentFlags & Flag) != 0; - set => _data.EquipmentFlags = (ushort)(value ? _data.EquipmentFlags | Flag : _data.EquipmentFlags & ~Flag); - } - - public bool ApplyStain - { - get => (_data.StainFlags & Flag) != 0; - set => _data.StainFlags = (ushort)(value ? _data.StainFlags | Flag : _data.StainFlags & ~Flag); - } - } - - public IEnumerable Equipment - => Enumerable.Range(0, EquipmentData.NumEquipment).Select(i => new Slot(this, i)); - - private void WriteEquipment(JObject obj) - { - var tok = new JObject(); - foreach (var slot in Equipment) - { - tok[SlotName] = new JObject - { - [nameof(Slot.ItemId)] = slot.ItemId, - [nameof(Slot.ApplyItem)] = slot.ApplyItem, - [nameof(Slot.StainId)] = slot.StainId.Value, - [nameof(Slot.ApplyStain)] = slot.ApplyStain, - }; - } - - obj[nameof(Equipment)] = tok; - } -} diff --git a/Glamourer/Saves/HumanDesign.cs b/Glamourer/Saves/HumanDesign.cs deleted file mode 100644 index 0dea537..0000000 --- a/Glamourer/Saves/HumanDesign.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using Glamourer.Customization; -using Newtonsoft.Json.Linq; -using CustomizeData = Penumbra.GameData.Structs.CustomizeData; - -namespace Glamourer.Saves; - -public partial class Design -{ - private CustomizeData _customizeData; - public CustomizeFlag CustomizeFlags { get; private set; } - - public Choice this[CustomizeIndex index] - => new(this, index); - - public unsafe Customize Customize - => new((CustomizeData*)Unsafe.AsPointer(ref _customizeData)); - - public readonly struct Choice - { - private readonly Design _data; - private readonly CustomizeFlag _flag; - private readonly CustomizeIndex _index; - - public Choice(Design design, CustomizeIndex index) - { - _data = design; - _index = index; - _flag = index.ToFlag(); - } - - public CustomizeValue Value - { - get => _data._customizeData.Get(_index); - set => _data._customizeData.Set(_index, value); - } - - public bool Apply - { - get => _data.CustomizeFlags.HasFlag(_flag); - set => _data.CustomizeFlags = value ? _data.CustomizeFlags | _flag : _data.CustomizeFlags & ~_flag; - } - - public CustomizeIndex Index - => _index; - } - - public IEnumerable Customization - => Enum.GetValues().Select(index => new Choice(this, index)); - - - public IEnumerable ActiveCustomizations - => Customization.Where(c => c.Apply); - - private void WriteCustomization(JObject obj) - { - var tok = new JObject(); - foreach (var choice in Customization) - tok[choice.Index.ToString()] = choice.Value.Value; - - obj[nameof(Customization)] = tok; - } -} diff --git a/Glamourer/State/ActiveDesign.StateManager.cs b/Glamourer/State/ActiveDesign.StateManager.cs new file mode 100644 index 0000000..4c4342b --- /dev/null +++ b/Glamourer/State/ActiveDesign.StateManager.cs @@ -0,0 +1,64 @@ +using System.Collections; +using Glamourer.Interop; +using Penumbra.GameData.Actors; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Glamourer.Designs; +using Penumbra.GameData.Structs; + +namespace Glamourer.State; + +public sealed partial class ActiveDesign +{ + public partial class Manager : IReadOnlyDictionary + { + private readonly ActorManager _actors; + + private readonly Dictionary _characterSaves = new(); + + public Manager(ActorManager actors) + => _actors = actors; + + public IEnumerator> GetEnumerator() + => _characterSaves.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _characterSaves.Count; + + public bool ContainsKey(ActorIdentifier key) + => _characterSaves.ContainsKey(key); + + public bool TryGetValue(ActorIdentifier key, [NotNullWhen(true)] out ActiveDesign? value) + => _characterSaves.TryGetValue(key, out value); + + public ActiveDesign this[ActorIdentifier key] + => _characterSaves[key]; + + public IEnumerable Keys + => _characterSaves.Keys; + + public IEnumerable Values + => _characterSaves.Values; + + public void DeleteSave(ActorIdentifier identifier) + => _characterSaves.Remove(identifier); + + public ActiveDesign GetOrCreateSave(Actor actor) + { + var id = actor.GetIdentifier(); + if (_characterSaves.TryGetValue(id, out var save)) + { + save.Update(actor); + return save; + } + + save = new ActiveDesign(); + save.Update(actor); + _characterSaves.Add(id.CreatePermanent(), save); + return save; + } + } +} diff --git a/Glamourer/State/ActiveDesign.cs b/Glamourer/State/ActiveDesign.cs new file mode 100644 index 0000000..32cf77c --- /dev/null +++ b/Glamourer/State/ActiveDesign.cs @@ -0,0 +1,93 @@ +using Glamourer.Customization; +using Glamourer.Designs; +using Glamourer.Interop; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; + +namespace Glamourer.State; + +public sealed partial class ActiveDesign : DesignBase +{ + private CharacterData _initialData = new(); + + private CustomizeFlag _changedCustomize; + private CustomizeFlag _fixedCustomize; + + private EquipFlag _changedEquip; + private EquipFlag _fixedEquip; + + public bool IsHatVisible { get; private set; } = false; + public bool IsWeaponVisible { get; private set; } = false; + public bool IsVisorToggled { get; private set; } = false; + public bool IsWet { get; private set; } = false; + + private ActiveDesign() + { } + + //public void ApplyToActor(Actor actor) + //{ + // if (!actor) + // return; + // + // void Redraw() + // => Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw); + // + // if (_drawData.ModelId != actor.ModelId) + // { + // Redraw(); + // return; + // } + // + // var customize1 = _drawData.Customize; + // var customize2 = actor.Customize; + // if (RedrawManager.NeedsRedraw(customize1, customize2)) + // { + // Redraw(); + // return; + // } + // + // Glamourer.RedrawManager.UpdateCustomize(actor, customize2); + // foreach (var slot in EquipSlotExtensions.EqdpSlots) + // Glamourer.RedrawManager.ChangeEquip(actor, slot, actor.Equip[slot]); + // Glamourer.RedrawManager.LoadWeapon(actor, actor.MainHand, actor.OffHand); + // if (actor.IsHuman && actor.DrawObject) + // RedrawManager.SetVisor(actor.DrawObject.Pointer, actor.VisorEnabled); + //} + // + public void Update(Actor actor) + { + if (!actor) + return; + + if (!_initialData.Customize.Equals(actor.Customize)) + { + _initialData.Customize.Load(actor.Customize); + Customize().Load(actor.Customize); + } + + var initialEquip = _initialData.Equipment; + var currentEquip = actor.Equip; + var equipment = Equipment(); + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var current = currentEquip[slot]; + if (initialEquip[slot] != current) + { + initialEquip[slot] = current; + equipment[slot] = current; + } + } + + if (_initialData.MainHand != actor.MainHand) + { + _initialData.MainHand = actor.MainHand; + UpdateMainhand(actor.MainHand); + } + + if (_initialData.OffHand != actor.OffHand) + { + _initialData.OffHand = actor.OffHand; + UpdateMainhand(actor.OffHand); + } + } +} diff --git a/Glamourer/State/CharacterSave.cs b/Glamourer/State/CharacterSave.cs deleted file mode 100644 index 44fe527..0000000 --- a/Glamourer/State/CharacterSave.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Glamourer.Customization; -using Glamourer.Interop; -using ImGuiScene; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Structs; -using Penumbra.String.Functions; -using CustomizeData = Penumbra.GameData.Structs.CustomizeData; - -namespace Glamourer.State; - -public class CharacterSaveConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, CharacterSave? value, JsonSerializer serializer) - { - var s = value?.ToBase64() ?? string.Empty; - serializer.Serialize(writer, s); - } - - public override CharacterSave ReadJson(JsonReader reader, Type objectType, CharacterSave? existingValue, bool hasExistingValue, - JsonSerializer serializer) - { - var token = JToken.Load(reader); - var s = token.ToObject(); - return CharacterSave.FromString(s!); - } -} - - - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public struct CharacterData -{ - public const byte CurrentVersion = 3; - - public uint ModelId; - public CustomizeData CustomizeData; - public CharacterWeapon MainHand; - public CharacterWeapon OffHand; - public CharacterArmor Head; - public CharacterArmor Body; - public CharacterArmor Hands; - public CharacterArmor Legs; - public CharacterArmor Feet; - public CharacterArmor Ears; - public CharacterArmor Neck; - public CharacterArmor Wrist; - public CharacterArmor RFinger; - public CharacterArmor LFinger; - - public unsafe Customize Customize - { - get - { - fixed (CustomizeData* ptr = &CustomizeData) - { - return new Customize(ptr); - } - } - } - - public unsafe CharacterEquip Equipment - { - get - { - fixed (CharacterArmor* ptr = &Head) - { - return new CharacterEquip(ptr); - } - } - } - - public static readonly CharacterData Default - = new() - { - ModelId = 0, - CustomizeData = Customize.Default, - MainHand = CharacterWeapon.Empty, - OffHand = CharacterWeapon.Empty, - Head = CharacterArmor.Empty, - Body = CharacterArmor.Empty, - Hands = CharacterArmor.Empty, - Legs = CharacterArmor.Empty, - Feet = CharacterArmor.Empty, - Ears = CharacterArmor.Empty, - Neck = CharacterArmor.Empty, - Wrist = CharacterArmor.Empty, - RFinger = CharacterArmor.Empty, - LFinger = CharacterArmor.Empty, - }; - - public unsafe CharacterData Clone() - { - var data = new CharacterData(); - fixed (void* ptr = &this) - { - MemoryUtility.MemCpyUnchecked(&data, ptr, sizeof(CharacterData)); - } - - return data; - } - - - public void Load(IDesignable designable) - { - ModelId = designable.ModelId; - Customize.Load(designable.Customize); - Equipment.Load(designable.Equip); - MainHand = designable.MainHand; - OffHand = designable.OffHand; - } -} - -[JsonConverter(typeof(CharacterSaveConverter))] -public class CharacterSave -{ - private CharacterData _data = CharacterData.Default; - - public CharacterSave() - { } - - public CharacterSave(Actor actor) - { - Load(actor); - } - - public void Load(T actor) where T : IDesignable - { - _data.Load(actor); - } - - public string ToBase64() - => string.Empty; - - public Customize Customize - => _data.Customize; - - public CharacterEquip Equipment - => _data.Equipment; - - public ref CharacterWeapon MainHand - => ref _data.MainHand; - - public ref CharacterWeapon OffHand - => ref _data.OffHand; - - public static CharacterSave FromString(string data) - => new(); -} diff --git a/Glamourer/State/CurrentDesign.cs b/Glamourer/State/CurrentDesign.cs deleted file mode 100644 index c85cedd..0000000 --- a/Glamourer/State/CurrentDesign.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Glamourer.Interop; -using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; - -namespace Glamourer.State; - -public unsafe class CurrentDesign : IDesign -{ - public ref CharacterData Data - => ref _drawData; - - private CharacterData _drawData; - private CharacterData _initialData; - - public CurrentDesign(Actor actor) - { - _initialData = new CharacterData(); - if (!actor) - return; - - _initialData.Load(actor); - var drawObject = actor.DrawObject; - if (drawObject.Valid) - _drawData.Load(drawObject); - else - _drawData = _initialData.Clone(); - } - - public void Reset() - => _drawData = _initialData; - - public void ApplyToActor(Actor actor) - { - if (!actor) - return; - - void Redraw() - => Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw); - - if (_drawData.ModelId != actor.ModelId) - { - Redraw(); - return; - } - - var customize1 = _drawData.Customize; - var customize2 = actor.Customize; - if (RedrawManager.NeedsRedraw(customize1, customize2)) - { - Redraw(); - return; - } - - Glamourer.RedrawManager.UpdateCustomize(actor, customize2); - foreach (var slot in EquipSlotExtensions.EqdpSlots) - Glamourer.RedrawManager.ChangeEquip(actor, slot, actor.Equip[slot]); - Glamourer.RedrawManager.LoadWeapon(actor, actor.MainHand, actor.OffHand); - if (actor.IsHuman && actor.DrawObject) - RedrawManager.SetVisor(actor.DrawObject.Pointer, actor.VisorEnabled); - } - - public void Update(Actor actor) - { - if (!actor) - return; - - if (!_initialData.Customize.Equals(actor.Customize)) - { - _initialData.Customize.Load(actor.Customize); - _drawData.Customize.Load(actor.Customize); - } - - var initialEquip = _initialData.Equipment; - var currentEquip = actor.Equip; - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - var current = currentEquip[slot]; - if (initialEquip[slot] != current) - { - initialEquip[slot] = current; - _drawData.Equipment[slot] = current; - } - } - - if (_initialData.MainHand != actor.MainHand) - { - _initialData.MainHand = actor.MainHand; - _drawData.MainHand = actor.MainHand; - } - - if (_initialData.OffHand != actor.OffHand) - { - _initialData.OffHand = actor.OffHand; - _drawData.OffHand = actor.OffHand; - } - } -} diff --git a/Glamourer/State/CurrentManipulations.cs b/Glamourer/State/CurrentManipulations.cs index fba9be5..cbc0e5b 100644 --- a/Glamourer/State/CurrentManipulations.cs +++ b/Glamourer/State/CurrentManipulations.cs @@ -6,11 +6,11 @@ using Penumbra.GameData.Actors; namespace Glamourer.State; -public class CurrentManipulations : IReadOnlyCollection> +public class CurrentManipulations : IReadOnlyCollection> { - private readonly Dictionary _characterSaves = new(); + private readonly Dictionary _characterSaves = new(); - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() => _characterSaves.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() @@ -19,7 +19,7 @@ public class CurrentManipulations : IReadOnlyCollection _characterSaves.Count; - public CurrentDesign GetOrCreateSave(Actor actor) + public ActiveDesign GetOrCreateSave(Actor actor) { var id = actor.GetIdentifier(); if (_characterSaves.TryGetValue(id, out var save)) @@ -28,7 +28,7 @@ public class CurrentManipulations : IReadOnlyCollection _characterSaves.Remove(identifier); - public bool TryGetDesign(ActorIdentifier identifier, [NotNullWhen(true)] out CurrentDesign? save) + public bool TryGetDesign(ActorIdentifier identifier, [NotNullWhen(true)] out ActiveDesign? save) => _characterSaves.TryGetValue(identifier, out save); //public CharacterArmor? ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data) diff --git a/Glamourer/State/FixedDesigns.cs b/Glamourer/State/FixedDesigns.cs index af5a55c..6327c05 100644 --- a/Glamourer/State/FixedDesigns.cs +++ b/Glamourer/State/FixedDesigns.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Glamourer.Designs; using Glamourer.Interop; using Penumbra.GameData.Actors; @@ -6,7 +7,7 @@ namespace Glamourer.State; public class FixedDesigns { - public bool TryGetDesign(ActorIdentifier actor, [NotNullWhen(true)] out CharacterSave? save) + public bool TryGetDesign(ActorIdentifier actor, [NotNullWhen(true)] out Design? save) { save = null; return false; diff --git a/Glamourer/State/IDesign.cs b/Glamourer/State/IDesign.cs index 8e0e736..d6cc18c 100644 --- a/Glamourer/State/IDesign.cs +++ b/Glamourer/State/IDesign.cs @@ -1,4 +1,5 @@ -using Glamourer.Interop; +using Glamourer.Designs; +using Glamourer.Interop; namespace Glamourer.State; diff --git a/Glamourer/Util/CustomizeExtensions.cs b/Glamourer/Util/CustomizeExtensions.cs index f0108dc..f0aafcf 100644 --- a/Glamourer/Util/CustomizeExtensions.cs +++ b/Glamourer/Util/CustomizeExtensions.cs @@ -134,6 +134,6 @@ public static unsafe class CustomizeExtensions return; foreach (var slot in EquipSlotExtensions.EqdpSlots) - (_, equip[slot]) = Glamourer.RestrictedGear.ResolveRestricted(equip[slot], slot, race, gender); + (_, equip[slot]) = Glamourer.Items.RestrictedGear.ResolveRestricted(equip[slot], slot, race, gender); } } diff --git a/Glamourer/Util/ItemManager.cs b/Glamourer/Util/ItemManager.cs new file mode 100644 index 0000000..1cc3dcb --- /dev/null +++ b/Glamourer/Util/ItemManager.cs @@ -0,0 +1,161 @@ +using System; +using System.Linq; +using Dalamud.Data; +using Dalamud.Plugin; +using Dalamud.Utility; +using Lumina.Excel; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Util; + +public class ItemManager : IDisposable +{ + public const string Nothing = "Nothing"; + public const string SmallClothesNpc = "Smallclothes (NPC)"; + public const ushort SmallClothesNpcModel = 9903; + + public readonly IObjectIdentifier Identifier; + public readonly ExcelSheet ItemSheet; + public readonly StainData Stains; + public readonly ItemData Items; + public readonly RestrictedGear RestrictedGear; + + public ItemManager(DalamudPluginInterface pi, DataManager gameData) + { + ItemSheet = gameData.GetExcelSheet()!; + Identifier = Penumbra.GameData.GameData.GetIdentifier(pi, gameData, gameData.Language); + Stains = new StainData(pi, gameData, gameData.Language); + Items = new ItemData(pi, gameData, gameData.Language); + RestrictedGear = new RestrictedGear(pi, gameData.Language, gameData); + DefaultSword = ItemSheet.GetRow(1601)!; // Weathered Shortsword + } + + public void Dispose() + { + Stains.Dispose(); + Items.Dispose(); + Identifier.Dispose(); + RestrictedGear.Dispose(); + } + + public readonly Item DefaultSword; + + public static uint NothingId(EquipSlot slot) + => uint.MaxValue - 128 - (uint)slot.ToSlot(); + + public static uint SmallclothesId(EquipSlot slot) + => uint.MaxValue - 256 - (uint)slot.ToSlot(); + + public static uint NothingId(FullEquipType type) + => uint.MaxValue - 384 - (uint)type; + + public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Item? item = null) + { + slot = slot.ToSlot(); + if (itemId == NothingId(slot)) + return (true, 0, 0, Nothing); + if (itemId == SmallclothesId(slot)) + return (true, SmallClothesNpcModel, 1, SmallClothesNpc); + + if (item == null || item.RowId != itemId) + item = ItemSheet.GetRow(itemId); + + if (item == null) + return (false, 0, 0, string.Intern($"Unknown #{itemId}")); + if (item.ToEquipType().ToSlot() != slot) + return (false, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})")); + + return (true, (SetId)item.ModelMain, (byte)(item.ModelMain >> 16), string.Intern(item.Name.ToDalamudString().TextValue)); + } + + public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Item? item = null) + { + if (item == null || item.RowId != itemId) + item = ItemSheet.GetRow(itemId); + + if (item == null) + return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown); + + var type = item.ToEquipType(); + if (type.ToSlot() != EquipSlot.MainHand) + return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type); + + return (true, (SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32), + string.Intern(item.Name.ToDalamudString().TextValue), type); + } + + public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, + FullEquipType mainType, Item? item = null) + { + var offType = mainType.Offhand(); + if (itemId == NothingId(offType)) + return (true, 0, 0, 0, Nothing, offType); + + if (item == null || item.RowId != itemId) + item = ItemSheet.GetRow(itemId); + + if (item == null) + return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown); + + + var type = item.ToEquipType(); + if (offType != type) + return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type); + + var (m, w, v) = offType.ToSlot() == EquipSlot.MainHand + ? ((SetId)item.ModelSub, (WeaponType)(item.ModelSub >> 16), (byte)(item.ModelSub >> 32)) + : ((SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32)); + + return (true, m, w, v, string.Intern(item.Name.ToDalamudString().TextValue), type); + } + + public (bool Valid, uint ItemId, string ItemName) Identify(EquipSlot slot, SetId id, byte variant) + { + slot = slot.ToSlot(); + if (!slot.IsEquipmentPiece()) + return (false, 0, string.Intern($"Unknown ({id.Value}-{variant})")); + + switch (id.Value) + { + case 0: return (true, NothingId(slot), Nothing); + case SmallClothesNpcModel: return (true, SmallclothesId(slot), SmallClothesNpc); + default: + var item = Identifier.Identify(id, variant, slot).FirstOrDefault(); + return item == null + ? (false, 0, string.Intern($"Unknown ({id.Value}-{variant})")) + : (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue)); + } + } + + public (bool Valid, uint ItemId, string ItemName, FullEquipType Type) Identify(EquipSlot slot, SetId id, WeaponType type, byte variant, + FullEquipType mainhandType = FullEquipType.Unknown) + { + switch (slot) + { + case EquipSlot.MainHand: + { + var item = Identifier.Identify(id, type, variant, slot).FirstOrDefault(); + return item != null + ? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType()) + : (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), mainhandType); + } + case EquipSlot.OffHand: + { + var weaponType = mainhandType.Offhand(); + if (id.Value == 0) + return (true, NothingId(weaponType), Nothing, weaponType); + + var item = Identifier.Identify(id, type, variant, slot).FirstOrDefault(); + return item != null + ? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType()) + : (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), + weaponType); + } + default: return (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), FullEquipType.Unknown); + } + } +}