From f8e9cc8988d002a7f3ec530205547d1cf08da8c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 7 Jul 2023 20:24:44 +0200 Subject: [PATCH] . --- .../Customization/CustomizationManager.cs | 3 - .../Customization/CustomizationOptions.cs | 3 - .../Customization/CustomizationSet.cs | 2 +- .../Customization/CustomizeFlag.cs | 1 + .../Customization/ICustomizationManager.cs | 1 - Glamourer/Api/GlamourerIpc.Apply.cs | 46 +- Glamourer/Api/GlamourerIpc.cs | 15 +- Glamourer/Automation/AutoDesign.cs | 2 +- Glamourer/Automation/AutoDesignApplier.cs | 58 ++- Glamourer/Automation/AutoDesignManager.cs | 2 +- Glamourer/Designs/Design.cs | 476 +++--------------- Glamourer/Designs/DesignBase.cs | 375 ++++++++++++++ Glamourer/Designs/DesignConverter.cs | 121 +++++ Glamourer/Designs/DesignFileSystem.cs | 27 +- Glamourer/Designs/DesignManager.cs | 166 +++++- Glamourer/Events/AutomationChanged.cs | 5 +- Glamourer/Events/DesignChanged.cs | 11 +- Glamourer/Events/StateChanged.cs | 1 + Glamourer/Glamourer.cs | 1 - Glamourer/Gui/Colors.cs | 2 + .../Customization/CustomizationDrawer.Icon.cs | 9 +- Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 103 +++- Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs | 9 +- Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs | 4 +- .../Gui/Tabs/AutomationTab/SetSelector.cs | 6 +- Glamourer/Gui/Tabs/DebugTab.cs | 154 ++++-- .../Gui/Tabs/DesignTab/DesignDetailTab.cs | 173 +++++++ .../DesignTab/DesignFileSystemSelector.cs | 107 +++- Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 358 +++++++++++-- .../Gui/Tabs/DesignTab/ModAssociationsTab.cs | 144 ++++++ Glamourer/Gui/Tabs/DesignTab/ModCombo.cs | 85 ++++ .../Gui/Tabs/UnlocksTab/UnlockOverview.cs | 11 - Glamourer/Interop/MetaService.cs | 7 +- Glamourer/Interop/ObjectManager.cs | 59 ++- Glamourer/Interop/Penumbra/PenumbraService.cs | 157 +++++- Glamourer/Services/BackupService.cs | 14 +- Glamourer/Services/ConfigMigrationService.cs | 5 +- Glamourer/Services/ServiceManager.cs | 9 +- Glamourer/State/ActorState.cs | 6 +- Glamourer/State/StateListener.cs | 21 +- Glamourer/State/StateManager.cs | 57 ++- Glamourer/Unlocks/ItemUnlockManager.cs | 12 +- Glamourer/Utility/CompressExtensions.cs | 55 ++ 43 files changed, 2215 insertions(+), 668 deletions(-) create mode 100644 Glamourer/Designs/DesignBase.cs create mode 100644 Glamourer/Designs/DesignConverter.cs create mode 100644 Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs create mode 100644 Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs create mode 100644 Glamourer/Gui/Tabs/DesignTab/ModCombo.cs create mode 100644 Glamourer/Utility/CompressExtensions.cs diff --git a/Glamourer.GameData/Customization/CustomizationManager.cs b/Glamourer.GameData/Customization/CustomizationManager.cs index ec48863..6f2460d 100644 --- a/Glamourer.GameData/Customization/CustomizationManager.cs +++ b/Glamourer.GameData/Customization/CustomizationManager.cs @@ -33,9 +33,6 @@ public class CustomizationManager : ICustomizationManager public ImGuiScene.TextureWrap GetIcon(uint iconId) => _options!.GetIcon(iconId); - public void RemoveIcon(uint iconId) - => _options!.RemoveIcon(iconId); - public string GetName(CustomName name) => _options!.GetName(name); } diff --git a/Glamourer.GameData/Customization/CustomizationOptions.cs b/Glamourer.GameData/Customization/CustomizationOptions.cs index bfc5d78..e4bcc4d 100644 --- a/Glamourer.GameData/Customization/CustomizationOptions.cs +++ b/Glamourer.GameData/Customization/CustomizationOptions.cs @@ -38,9 +38,6 @@ public partial class CustomizationOptions internal ImGuiScene.TextureWrap GetIcon(uint id) => _icons.LoadIcon(id); - internal void RemoveIcon(uint id) - => _icons.RemoveIcon(id); - private readonly IconStorage _icons; private static readonly int ListSize = Clans.Length * Genders.Length; diff --git a/Glamourer.GameData/Customization/CustomizationSet.cs b/Glamourer.GameData/Customization/CustomizationSet.cs index c1160d7..a84666c 100644 --- a/Glamourer.GameData/Customization/CustomizationSet.cs +++ b/Glamourer.GameData/Customization/CustomizationSet.cs @@ -249,7 +249,7 @@ public class CustomizationSet _ => index switch { CustomizeIndex.Face => Faces.Count, - CustomizeIndex.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face.Value].Count : 0, + CustomizeIndex.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face.Value].Count : HairStyles.Count, CustomizeIndex.SkinColor => SkinColors.Count, CustomizeIndex.EyeColorRight => EyeColors.Count, CustomizeIndex.HairColor => HairColors.Count, diff --git a/Glamourer.GameData/Customization/CustomizeFlag.cs b/Glamourer.GameData/Customization/CustomizeFlag.cs index 52964ca..54f7f5c 100644 --- a/Glamourer.GameData/Customization/CustomizeFlag.cs +++ b/Glamourer.GameData/Customization/CustomizeFlag.cs @@ -48,6 +48,7 @@ public enum CustomizeFlag : ulong public static class CustomizeFlagExtensions { public const CustomizeFlag All = (CustomizeFlag)(((ulong)CustomizeFlag.FacePaintColor << 1) - 1ul); + public const CustomizeFlag AllRelevant = All & ~CustomizeFlag.BodyType & ~CustomizeFlag.Race; public const CustomizeFlag RedrawRequired = CustomizeFlag.Race | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.Face | CustomizeFlag.BodyType; public static bool RequiresRedraw(this CustomizeFlag flags) diff --git a/Glamourer.GameData/Customization/ICustomizationManager.cs b/Glamourer.GameData/Customization/ICustomizationManager.cs index c1a1a1a..6e1cfe3 100644 --- a/Glamourer.GameData/Customization/ICustomizationManager.cs +++ b/Glamourer.GameData/Customization/ICustomizationManager.cs @@ -12,6 +12,5 @@ public interface ICustomizationManager public CustomizationSet GetList(SubRace race, Gender gender); public ImGuiScene.TextureWrap GetIcon(uint iconId); - public void RemoveIcon(uint iconId); public string GetName(CustomName name); } diff --git a/Glamourer/Api/GlamourerIpc.Apply.cs b/Glamourer/Api/GlamourerIpc.Apply.cs index b3b20f7..c39b58d 100644 --- a/Glamourer/Api/GlamourerIpc.Apply.cs +++ b/Glamourer/Api/GlamourerIpc.Apply.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; using Glamourer.Designs; @@ -45,24 +44,24 @@ public partial class GlamourerIpc public void ApplyAll(string base64, string characterName) - => ApplyDesign(CreateTemporaryFromBase64(base64, true, true), FindActors(characterName)); + => ApplyDesign(_designConverter.FromBase64(base64, true, true), FindActors(characterName)); public void ApplyAllToCharacter(string base64, Character? character) - => ApplyDesign(CreateTemporaryFromBase64(base64, true, true), FindActors(character)); + => ApplyDesign(_designConverter.FromBase64(base64, true, true), FindActors(character)); public void ApplyOnlyEquipment(string base64, string characterName) - => ApplyDesign(CreateTemporaryFromBase64(base64, false, true), FindActors(characterName)); + => ApplyDesign(_designConverter.FromBase64(base64, false, true), FindActors(characterName)); public void ApplyOnlyEquipmentToCharacter(string base64, Character? character) - => ApplyDesign(CreateTemporaryFromBase64(base64, false, true), FindActors(character)); + => ApplyDesign(_designConverter.FromBase64(base64, false, true), FindActors(character)); public void ApplyOnlyCustomization(string base64, string characterName) - => ApplyDesign(CreateTemporaryFromBase64(base64, true, false), FindActors(characterName)); + => ApplyDesign(_designConverter.FromBase64(base64, true, false), FindActors(characterName)); public void ApplyOnlyCustomizationToCharacter(string base64, Character? character) - => ApplyDesign(CreateTemporaryFromBase64(base64, true, false), FindActors(character)); + => ApplyDesign(_designConverter.FromBase64(base64, true, false), FindActors(character)); - private void ApplyDesign(Design? design, IEnumerable actors) + private void ApplyDesign(DesignBase? design, IEnumerable actors) { if (design == null) return; @@ -80,33 +79,4 @@ public partial class GlamourerIpc _stateManager.ApplyDesign(design, state); } } - - private Design? CreateTemporaryFromBase64(string base64, bool customize, bool equip) - { - try - { - var ret = new Design(_items); - ret.MigrateBase64(_items, base64); - if (!customize) - { - ret.ApplyCustomize = 0; - ret.SetApplyWetness(false); - } - - if (!equip) - { - ret.ApplyEquip = 0; - ret.SetApplyHatVisible(false); - ret.SetApplyWeaponVisible(false); - ret.SetApplyVisorToggle(false); - } - - return ret; - } - catch (Exception ex) - { - Glamourer.Log.Error($"[IPC] Could not parse base64 string [{base64}]:\n{ex}"); - return null; - } - } } diff --git a/Glamourer/Api/GlamourerIpc.cs b/Glamourer/Api/GlamourerIpc.cs index 1f13781..d3a2c83 100644 --- a/Glamourer/Api/GlamourerIpc.cs +++ b/Glamourer/Api/GlamourerIpc.cs @@ -3,6 +3,7 @@ using Dalamud.Plugin; using System; using System.Collections.Generic; using System.Linq; +using Glamourer.Designs; using Glamourer.Interop; using Glamourer.Services; using Glamourer.State; @@ -17,17 +18,21 @@ public partial class GlamourerIpc : IDisposable public const int CurrentApiVersionMajor = 0; public const int CurrentApiVersionMinor = 1; - private readonly StateManager _stateManager; - private readonly ObjectManager _objects; - private readonly ActorService _actors; - private readonly ItemManager _items; + private readonly StateManager _stateManager; + private readonly ObjectManager _objects; + private readonly ActorService _actors; + private readonly ItemManager _items; + private readonly DesignConverter _designConverter; - public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorService actors, ItemManager items) + + public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorService actors, ItemManager items, + DesignConverter designConverter) { _stateManager = stateManager; _objects = objects; _actors = actors; _items = items; + _designConverter = designConverter; _apiVersionProvider = new FuncProvider(pi, LabelApiVersion, ApiVersion); _apiVersionsProvider = new FuncProvider<(int Major, int Minor)>(pi, LabelApiVersions, ApiVersions); diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs index 79f7d36..18deaf9 100644 --- a/Glamourer/Automation/AutoDesign.cs +++ b/Glamourer/Automation/AutoDesign.cs @@ -21,7 +21,7 @@ public class AutoDesign All = Armor | Accessories | Customizations | Weapons | Stains, } - public Design Design; + public Design Design = null!; public JobGroup Jobs; public Type ApplicationType; diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 8a335b4..bb15772 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -25,9 +25,12 @@ public class AutoDesignApplier : IDisposable private readonly CustomizationService _customizations; private readonly CustomizeUnlockManager _customizeUnlocks; private readonly ItemUnlockManager _itemUnlocks; + private readonly AutomationChanged _event; + private readonly ObjectManager _objects; public AutoDesignApplier(Configuration config, AutoDesignManager manager, CodeService code, StateManager state, JobService jobs, - CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks) + CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, + AutomationChanged @event, ObjectManager objects) { _config = config; _manager = manager; @@ -38,14 +41,59 @@ public class AutoDesignApplier : IDisposable _actors = actors; _itemUnlocks = itemUnlocks; _customizeUnlocks = customizeUnlocks; + _event = @event; + _objects = objects; _jobs.JobChanged += OnJobChange; + _event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier); } public void Dispose() { + _event.Unsubscribe(OnAutomationChange); _jobs.JobChanged -= OnJobChange; } + private void OnAutomationChange(AutomationChanged.Type type, AutoDesignSet? set, object? _) + { + if (!_config.EnableAutoDesigns || set is not { Enabled: true }) + return; + + switch (type) + { + case AutomationChanged.Type.ChangeIdentifier: + case AutomationChanged.Type.ToggleSet: + case AutomationChanged.Type.AddedDesign: + case AutomationChanged.Type.DeletedDesign: + case AutomationChanged.Type.MovedDesign: + case AutomationChanged.Type.ChangedDesign: + case AutomationChanged.Type.ChangedConditions: + _objects.Update(); + if (_objects.TryGetValue(set.Identifier, out var data)) + { + if (_state.GetOrCreate(set.Identifier, data.Objects[0], out var state)) + { + Reduce(data.Objects[0], state, set, false); + foreach (var actor in data.Objects) + _state.ReapplyState(actor); + } + } + else if (_objects.TryGetValueAllWorld(set.Identifier, out data)) + { + foreach (var actor in data.Objects) + { + var id = actor.GetIdentifier(_actors.AwaitedService); + if (_state.GetOrCreate(id, actor, out var state)) + { + Reduce(actor, state, set, false); + _state.ReapplyState(actor); + } + } + } + + break; + } + } + private void OnJobChange(Actor actor, Job _) { if (!_config.EnableAutoDesigns || !actor.Identifier(_actors.AwaitedService, out var id)) @@ -242,28 +290,28 @@ public class AutoDesignApplier : IDisposable { if (applyHat && (totalMetaFlags & 0x01) == 0) { - if (!respectManual || state[ActorState.MetaFlag.HatState] is not StateChanged.Source.Manual) + if (!respectManual || state[ActorState.MetaIndex.HatState] is not StateChanged.Source.Manual) _state.ChangeHatState(state, design.IsHatVisible(), StateChanged.Source.Fixed); totalMetaFlags |= 0x01; } if (applyVisor && (totalMetaFlags & 0x02) == 0) { - if (!respectManual || state[ActorState.MetaFlag.VisorState] is not StateChanged.Source.Manual) + if (!respectManual || state[ActorState.MetaIndex.VisorState] is not StateChanged.Source.Manual) _state.ChangeVisorState(state, design.IsVisorToggled(), StateChanged.Source.Fixed); totalMetaFlags |= 0x02; } if (applyWeapon && (totalMetaFlags & 0x04) == 0) { - if (!respectManual || state[ActorState.MetaFlag.WeaponState] is not StateChanged.Source.Manual) + if (!respectManual || state[ActorState.MetaIndex.WeaponState] is not StateChanged.Source.Manual) _state.ChangeWeaponState(state, design.IsWeaponVisible(), StateChanged.Source.Fixed); totalMetaFlags |= 0x04; } if (applyWet && (totalMetaFlags & 0x08) == 0) { - if (!respectManual || state[ActorState.MetaFlag.Wetness] is not StateChanged.Source.Manual) + if (!respectManual || state[ActorState.MetaIndex.Wetness] is not StateChanged.Source.Manual) _state.ChangeWetness(state, design.IsWet(), StateChanged.Source.Fixed); totalMetaFlags |= 0x08; } diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index 09239e6..0b31265 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -29,7 +29,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList private readonly DesignManager _designs; private readonly ActorService _actors; private readonly AutomationChanged _event; - private readonly ItemUnlockManager _unlockManager; + private readonly ItemUnlockManager _unlockManager; private readonly List _data = new(); private readonly Dictionary _enabled = new(); diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 86f8ae2..6f2e60e 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -1,279 +1,95 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Interface.Internal.Notifications; -using Glamourer.Customization; +using Glamourer.Interop.Penumbra; using Glamourer.Services; -using Glamourer.Structs; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; namespace Glamourer.Designs; -public class Design : ISavable +public sealed class Design : DesignBase, ISavable { #region Data internal Design(ItemManager items) + : base(items) + { } + + internal Design(DesignBase other) + : base(other) + { } + + internal Design(Design other) + : base(other) { - DesignData.SetDefaultEquipment(items); + Tags = Tags.ToArray(); + Description = Description; + AssociatedMods = new SortedList(other.AssociatedMods); } // Metadata - public const int FileVersion = 1; + public new const int FileVersion = 1; - public Guid Identifier { get; internal init; } - public DateTimeOffset CreationDate { get; internal init; } - public DateTimeOffset LastEdit { get; internal set; } - public LowerString Name { get; internal set; } = LowerString.Empty; - public string Description { get; internal set; } = string.Empty; - public string[] Tags { get; internal set; } = Array.Empty(); - public int Index { get; internal set; } - - internal DesignData DesignData; + public Guid Identifier { get; internal init; } + public DateTimeOffset CreationDate { get; internal init; } + public DateTimeOffset LastEdit { get; internal set; } + public LowerString Name { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string[] Tags { get; internal set; } = Array.Empty(); + public int Index { get; internal set; } + public SortedList AssociatedMods { get; private set; } = new(); public string Incognito => Identifier.ToString()[..8]; - /// Unconditionally apply a design to a designdata. - /// Whether a redraw is required for the changes to take effect. - public (bool, CustomizeFlag, EquipFlag) ApplyDesign(ref DesignData data) - { - var modelChanged = data.ModelId != DesignData.ModelId; - data.ModelId = DesignData.ModelId; - - CustomizeFlag customizeFlags = 0; - foreach (var index in Enum.GetValues()) - { - if (!DoApplyCustomize(index)) - continue; - - if (data.Customize.Set(index, DesignData.Customize[index])) - customizeFlags |= index.ToFlag(); - } - - EquipFlag equipFlags = 0; - foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)) - { - if (DoApplyEquip(slot)) - if (data.SetItem(slot, DesignData.Item(slot))) - equipFlags |= slot.ToFlag(); - - if (DoApplyStain(slot)) - if (data.SetStain(slot, DesignData.Stain(slot))) - equipFlags |= slot.ToStainFlag(); - } - - if (DoApplyHatVisible()) - data.SetHatVisible(DesignData.IsHatVisible()); - - if (DoApplyVisorToggle()) - data.SetVisor(DesignData.IsVisorToggled()); - - if (DoApplyWeaponVisible()) - data.SetWeaponVisible(DesignData.IsWeaponVisible()); - - if (DoApplyWetness()) - data.SetIsWet(DesignData.IsWet()); - return (modelChanged, customizeFlags, equipFlags); - } - #endregion - #region Application Data - - [Flags] - private enum DesignFlags : byte - { - ApplyHatVisible = 0x01, - ApplyVisorState = 0x02, - ApplyWeaponVisible = 0x04, - ApplyWetness = 0x08, - WriteProtected = 0x10, - } - - internal CustomizeFlag ApplyCustomize = CustomizeFlagExtensions.All; - internal EquipFlag ApplyEquip = EquipFlagExtensions.All; - private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; - - public bool DoApplyHatVisible() - => _designFlags.HasFlag(DesignFlags.ApplyHatVisible); - - public bool DoApplyVisorToggle() - => _designFlags.HasFlag(DesignFlags.ApplyVisorState); - - public bool DoApplyWeaponVisible() - => _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible); - - public bool DoApplyWetness() - => _designFlags.HasFlag(DesignFlags.ApplyWetness); - - public bool WriteProtected() - => _designFlags.HasFlag(DesignFlags.WriteProtected); - - public bool SetApplyHatVisible(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; - return true; - } - - public bool SetApplyVisorToggle(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; - return true; - } - - public bool SetApplyWeaponVisible(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; - return true; - } - - public bool SetApplyWetness(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyWetness : _designFlags & ~DesignFlags.ApplyWetness; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; - return true; - } - - public bool SetWriteProtected(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; - return true; - } - - - 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()); - - internal bool SetApplyEquip(EquipSlot slot, bool value) - { - var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag(); - if (newValue == ApplyEquip) - return false; - - ApplyEquip = newValue; - return true; - } - - internal bool SetApplyStain(EquipSlot slot, bool value) - { - var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag(); - if (newValue == ApplyEquip) - return false; - - ApplyEquip = newValue; - return true; - } - - internal bool SetApplyCustomize(CustomizeIndex idx, bool value) - { - var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag(); - if (newValue == ApplyCustomize) - return false; - - ApplyCustomize = newValue; - return true; - } - - #endregion #region Serialization - private JObject JsonSerialize() - { - var ret = new JObject - { - ["FileVersion"] = FileVersion, - ["Identifier"] = Identifier, - ["CreationDate"] = CreationDate, - ["LastEdit"] = LastEdit, - ["Name"] = Name.Text, - ["Description"] = Description, - ["Tags"] = JArray.FromObject(Tags), - ["WriteProtected"] = WriteProtected(), - ["Equipment"] = SerializeEquipment(), - ["Customize"] = SerializeCustomize(), - }; - return ret; - } - - private JObject SerializeEquipment() - { - static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain) - => new() - { - ["ItemId"] = itemId, - ["Stain"] = stain.Value, - ["Apply"] = apply, - ["ApplyStain"] = applyStain, - }; - - var ret = new JObject(); - - foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) - { - var item = DesignData.Item(slot); - var stain = DesignData.Stain(slot); - ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot)); - } - - ret["Hat"] = new QuadBool(DesignData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply"); - ret["Visor"] = new QuadBool(DesignData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply"); - ret["Weapon"] = new QuadBool(DesignData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply"); - - return ret; - } - - private JObject SerializeCustomize() + public new JObject JsonSerialize() { var ret = new JObject() - { - ["ModelId"] = DesignData.ModelId, - }; - var customize = DesignData.Customize; - foreach (var idx in Enum.GetValues()) - { - ret[idx.ToString()] = new JObject() { - ["Value"] = customize[idx].Value, - ["Apply"] = DoApplyCustomize(idx), - }; - } + ["FileVersion"] = FileVersion, + ["Identifier"] = Identifier, + ["CreationDate"] = CreationDate, + ["LastEdit"] = LastEdit, + ["Name"] = Name.Text, + ["Description"] = Description, + ["Tags"] = JArray.FromObject(Tags), + ["WriteProtected"] = WriteProtected(), + ["Equipment"] = SerializeEquipment(), + ["Customize"] = SerializeCustomize(), + ["Mods"] = SerializeMods(), + } + ; + return ret; + } - ret["Wetness"] = new JObject() + private JArray SerializeMods() + { + var ret = new JArray(); + foreach (var (mod, settings) in AssociatedMods) { - ["Value"] = DesignData.IsWet(), - ["Apply"] = DoApplyWetness(), - }; + var obj = new JObject() + { + ["Name"] = mod.Name, + ["Directory"] = mod.DirectoryName, + ["Enabled"] = settings.Enabled, + }; + if (settings.Enabled) + { + obj["Priority"] = settings.Priority; + obj["Settings"] = JObject.FromObject(settings.Settings); + } + + ret.Add(obj); + } return ret; } @@ -287,8 +103,8 @@ public class Design : ISavable var version = json["FileVersion"]?.ToObject() ?? 0; return version switch { - 1 => LoadDesignV1(customizations, items, json), - _ => throw new Exception("The design to be loaded has no valid Version."), + FileVersion => LoadDesignV1(customizations, items, json), + _ => throw new Exception("The design to be loaded has no valid Version."), }; } @@ -314,128 +130,37 @@ public class Design : ISavable if (design.LastEdit < creationDate) design.LastEdit = creationDate; - LoadEquip(items, json["Equipment"], design); - LoadCustomize(customizations, json["Customize"], design); + LoadEquip(items, json["Equipment"], design, design.Name); + LoadCustomize(customizations, json["Customize"], design, design.Name); + LoadMods(json["Mods"], design); return design; } - private static void LoadEquip(ItemManager items, JToken? equip, Design design) + private static void LoadMods(JToken? mods, Design design) { - if (equip == null) - { - design.DesignData.SetDefaultEquipment(items); - Glamourer.Chat.NotificationMessage("The loaded design does not contain any equipment data, reset to default.", "Warning", - NotificationType.Warning); + if (mods is not JArray array) return; - } - static (uint, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item) + foreach (var tok in array) { - 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 name = tok["Name"]?.ToObject(); + var directory = tok["Directory"]?.ToObject(); + var enabled = tok["Enabled"]?.ToObject(); + if (name == null || directory == null || enabled == null) + { + Glamourer.Chat.NotificationMessage("The loaded design contains an invalid mod, skipped.", "Warning", NotificationType.Warning); + continue; + } + + var settingsDict = tok["Settings"]?.ToObject>() ?? new Dictionary(); + var settings = new SortedList>(settingsDict.Count); + foreach (var (key, value) in settingsDict) + settings.Add(key, value); + var priority = tok["Priority"]?.ToObject() ?? 0; + if (!design.AssociatedMods.TryAdd(new Mod(name, directory), new ModSettings(settings, priority, enabled.Value))) + Glamourer.Chat.NotificationMessage("The loaded design contains a mod more than once, skipped.", "Warning", + NotificationType.Warning); } - - void PrintWarning(string msg) - { - if (msg.Length > 0) - Glamourer.Chat.NotificationMessage($"{msg} ({design.Name})", "Warning", NotificationType.Warning); - } - - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]); - - PrintWarning(items.ValidateItem(slot, id, out var item)); - PrintWarning(items.ValidateStain(stain, out stain)); - design.DesignData.SetItem(slot, item); - design.DesignData.SetStain(slot, stain); - design.SetApplyEquip(slot, apply); - design.SetApplyStain(slot, applyStain); - } - - { - var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); - if (id == ItemManager.NothingId(EquipSlot.MainHand)) - id = items.DefaultSword.Id; - var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); - if (id == ItemManager.NothingId(EquipSlot.OffHand)) - id = ItemManager.NothingId(FullEquipType.Shield); - - PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off)); - PrintWarning(items.ValidateStain(stain, out stain)); - PrintWarning(items.ValidateStain(stainOff, out stainOff)); - design.DesignData.SetItem(EquipSlot.MainHand, main); - design.DesignData.SetItem(EquipSlot.OffHand, off); - design.DesignData.SetStain(EquipSlot.MainHand, stain); - design.DesignData.SetStain(EquipSlot.OffHand, stainOff); - design.SetApplyEquip(EquipSlot.MainHand, apply); - design.SetApplyEquip(EquipSlot.OffHand, applyOff); - design.SetApplyStain(EquipSlot.MainHand, applyStain); - design.SetApplyStain(EquipSlot.OffHand, applyStainOff); - } - var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse); - design.SetApplyHatVisible(metaValue.Enabled); - design.DesignData.SetHatVisible(metaValue.ForcedValue); - - metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse); - design.SetApplyWeaponVisible(metaValue.Enabled); - design.DesignData.SetWeaponVisible(metaValue.ForcedValue); - - metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); - design.SetApplyVisorToggle(metaValue.Enabled); - design.DesignData.SetVisor(metaValue.ForcedValue); - } - - private static void LoadCustomize(CustomizationService customizations, JToken? json, Design design) - { - if (json == null) - { - design.DesignData.ModelId = 0; - design.DesignData.Customize = Customize.Default; - Glamourer.Chat.NotificationMessage("The loaded design does not contain any customization data, reset to default.", "Warning", - NotificationType.Warning); - return; - } - - void PrintWarning(string msg) - { - if (msg.Length > 0) - Glamourer.Chat.NotificationMessage($"{msg} ({design.Name})", "Warning", NotificationType.Warning); - } - - design.DesignData.ModelId = json["ModelId"]?.ToObject() ?? 0; - PrintWarning(customizations.ValidateModelId(design.DesignData.ModelId, out design.DesignData.ModelId)); - - var race = (Race)(json[CustomizeIndex.Race.ToString()]?["Value"]?.ToObject() ?? 0); - var clan = (SubRace)(json[CustomizeIndex.Clan.ToString()]?["Value"]?.ToObject() ?? 0); - PrintWarning(customizations.ValidateClan(clan, race, out race, out clan)); - var gender = (Gender)((json[CustomizeIndex.Gender.ToString()]?["Value"]?.ToObject() ?? 0) + 1); - PrintWarning(customizations.ValidateGender(race, gender, out gender)); - design.DesignData.Customize.Race = race; - design.DesignData.Customize.Clan = clan; - design.DesignData.Customize.Gender = gender; - design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject() ?? false); - design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject() ?? false); - design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject() ?? false); - - var set = customizations.AwaitedService.GetList(clan, gender); - - foreach (var idx in Enum.GetValues().Where(set.IsAvailable)) - { - var tok = json[idx.ToString()]; - var data = (CustomizeValue)(tok?["Value"]?.ToObject() ?? 0); - PrintWarning(CustomizationService.ValidateCustomizeValue(set, design.DesignData.Customize.Face, idx, data, out data)); - var apply = tok?["Apply"]?.ToObject() ?? false; - design.DesignData.Customize[idx] = data; - design.SetApplyCustomize(idx, apply); - } - - var wetness = QuadBool.FromJObject(json["Wetness"], "Value", "Apply", QuadBool.NullFalse); - design.DesignData.SetIsWet(wetness.ForcedValue); - design.SetApplyWetness(wetness.Enabled); } #endregion @@ -459,39 +184,4 @@ public class Design : ISavable => Path.GetFileNameWithoutExtension(fileName); #endregion - - public void MigrateBase64(ItemManager items, string base64) - { - DesignData = DesignBase64Migration.MigrateBase64(items, base64, out var equipFlags, out var customizeFlags, out var writeProtected, - out var applyHat, out var applyVisor, out var applyWeapon); - ApplyEquip = equipFlags; - ApplyCustomize = customizeFlags; - SetWriteProtected(writeProtected); - SetApplyHatVisible(applyHat); - SetApplyVisorToggle(applyVisor); - SetApplyWeaponVisible(applyWeapon); - SetApplyWetness(DesignData.IsWet()); - } - - // - //public static Design CreateTemporaryFromBase64(ItemManager items, string base64, bool customize, bool equip) - //{ - // var ret = new Design(items); - // ret.MigrateBase64(items, base64); - // if (!customize) - // ret._applyCustomize = 0; - // if (!equip) - // ret._applyEquip = 0; - // ret.Wetness = ret.Wetness.SetEnabled(customize); - // ret.Visor = ret.Visor.SetEnabled(equip); - // ret.Hat = ret.Hat.SetEnabled(equip); - // ret.Weapon = ret.Weapon.SetEnabled(equip); - // return ret; - //} - - // Outdated. - //public string CreateOldBase64() - // => DesignBase64Migration.CreateOldBase64(in ModelData, _applyEquip, _applyCustomize, Wetness == QuadBool.True, Hat.ForcedValue, - // Hat.Enabled, - // Visor.ForcedValue, Visor.Enabled, Weapon.ForcedValue, Weapon.Enabled, WriteProtected, 1f); } diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs new file mode 100644 index 0000000..ce02c4f --- /dev/null +++ b/Glamourer/Designs/DesignBase.cs @@ -0,0 +1,375 @@ +using System; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using Glamourer.Customization; +using Glamourer.Services; +using Glamourer.Structs; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public class DesignBase +{ + public const int FileVersion = 1; + + internal DesignBase(ItemManager items) + { + DesignData.SetDefaultEquipment(items); + } + + internal DesignBase(DesignBase clone) + { + DesignData = clone.DesignData; + ApplyCustomize = clone.ApplyCustomize & CustomizeFlagExtensions.All; + ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All; + _designFlags = clone._designFlags & (DesignFlags) 0x0F; + } + + internal DesignData DesignData = new(); + + #region Application Data + + [Flags] + private enum DesignFlags : byte + { + ApplyHatVisible = 0x01, + ApplyVisorState = 0x02, + ApplyWeaponVisible = 0x04, + ApplyWetness = 0x08, + WriteProtected = 0x10, + } + + internal CustomizeFlag ApplyCustomize = CustomizeFlagExtensions.All; + internal EquipFlag ApplyEquip = EquipFlagExtensions.All; + private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; + + public bool DoApplyHatVisible() + => _designFlags.HasFlag(DesignFlags.ApplyHatVisible); + + public bool DoApplyVisorToggle() + => _designFlags.HasFlag(DesignFlags.ApplyVisorState); + + public bool DoApplyWeaponVisible() + => _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible); + + public bool DoApplyWetness() + => _designFlags.HasFlag(DesignFlags.ApplyWetness); + + public bool WriteProtected() + => _designFlags.HasFlag(DesignFlags.WriteProtected); + + public bool SetApplyHatVisible(bool value) + { + var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible; + if (newFlag == _designFlags) + return false; + + _designFlags = newFlag; + return true; + } + + public bool SetApplyVisorToggle(bool value) + { + var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState; + if (newFlag == _designFlags) + return false; + + _designFlags = newFlag; + return true; + } + + public bool SetApplyWeaponVisible(bool value) + { + var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible; + if (newFlag == _designFlags) + return false; + + _designFlags = newFlag; + return true; + } + + public bool SetApplyWetness(bool value) + { + var newFlag = value ? _designFlags | DesignFlags.ApplyWetness : _designFlags & ~DesignFlags.ApplyWetness; + if (newFlag == _designFlags) + return false; + + _designFlags = newFlag; + return true; + } + + public bool SetWriteProtected(bool value) + { + var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected; + if (newFlag == _designFlags) + return false; + + _designFlags = newFlag; + return true; + } + + public bool DoApplyEquip(EquipSlot slot) + => ApplyEquip.HasFlag(slot.ToFlag()); + + public bool DoApplyStain(EquipSlot slot) + => ApplyEquip.HasFlag(slot.ToStainFlag()); + + public bool DoApplyCustomize(CustomizeIndex idx) + => idx is not CustomizeIndex.Race and not CustomizeIndex.BodyType && ApplyCustomize.HasFlag(idx.ToFlag()); + + internal bool SetApplyEquip(EquipSlot slot, bool value) + { + var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag(); + if (newValue == ApplyEquip) + return false; + + ApplyEquip = newValue; + return true; + } + + internal bool SetApplyStain(EquipSlot slot, bool value) + { + var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag(); + if (newValue == ApplyEquip) + return false; + + ApplyEquip = newValue; + return true; + } + + internal bool SetApplyCustomize(CustomizeIndex idx, bool value) + { + var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag(); + if (newValue == ApplyCustomize) + return false; + + ApplyCustomize = newValue; + return true; + } + + #endregion + + #region Serialization + + public JObject JsonSerialize() + { + var ret = new JObject + { + ["FileVersion"] = FileVersion, + ["Equipment"] = SerializeEquipment(), + ["Customize"] = SerializeCustomize(), + }; + return ret; + } + + protected JObject SerializeEquipment() + { + static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain) + => new() + { + ["ItemId"] = itemId, + ["Stain"] = stain.Value, + ["Apply"] = apply, + ["ApplyStain"] = applyStain, + }; + + var ret = new JObject(); + + foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) + { + var item = DesignData.Item(slot); + var stain = DesignData.Stain(slot); + ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot)); + } + + ret["Hat"] = new QuadBool(DesignData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply"); + ret["Visor"] = new QuadBool(DesignData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply"); + ret["Weapon"] = new QuadBool(DesignData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply"); + + return ret; + } + + protected JObject SerializeCustomize() + { + var ret = new JObject() + { + ["ModelId"] = DesignData.ModelId, + }; + var customize = DesignData.Customize; + foreach (var idx in Enum.GetValues()) + { + ret[idx.ToString()] = new JObject() + { + ["Value"] = customize[idx].Value, + ["Apply"] = DoApplyCustomize(idx), + }; + } + + ret["Wetness"] = new JObject() + { + ["Value"] = DesignData.IsWet(), + ["Apply"] = DoApplyWetness(), + }; + + return ret; + } + + #endregion + + #region Deserialization + + public static DesignBase LoadDesignBase(CustomizationService customizations, ItemManager items, JObject json) + { + var version = json["FileVersion"]?.ToObject() ?? 0; + return version switch + { + FileVersion => LoadDesignV1Base(customizations, items, json), + _ => throw new Exception("The design to be loaded has no valid Version."), + }; + } + + private static DesignBase LoadDesignV1Base(CustomizationService customizations, ItemManager items, JObject json) + { + var ret = new DesignBase(items); + LoadEquip(items, json["Equipment"], ret, "Temporary Design"); + LoadCustomize(customizations, json["Customize"], ret, "Temporary Design"); + return ret; + } + + protected static void LoadEquip(ItemManager items, JToken? equip, DesignBase design, string name) + { + if (equip == null) + { + design.DesignData.SetDefaultEquipment(items); + Glamourer.Chat.NotificationMessage("The loaded design does not contain any equipment data, reset to default.", "Warning", + NotificationType.Warning); + return; + } + + 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); + } + + void PrintWarning(string msg) + { + if (msg.Length > 0) + Glamourer.Chat.NotificationMessage($"{msg} ({name})", "Warning", NotificationType.Warning); + } + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]); + + PrintWarning(items.ValidateItem(slot, id, out var item)); + PrintWarning(items.ValidateStain(stain, out stain)); + design.DesignData.SetItem(slot, item); + design.DesignData.SetStain(slot, stain); + design.SetApplyEquip(slot, apply); + design.SetApplyStain(slot, applyStain); + } + + { + var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); + if (id == ItemManager.NothingId(EquipSlot.MainHand)) + id = items.DefaultSword.Id; + var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); + if (id == ItemManager.NothingId(EquipSlot.OffHand)) + id = ItemManager.NothingId(FullEquipType.Shield); + + PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off)); + PrintWarning(items.ValidateStain(stain, out stain)); + PrintWarning(items.ValidateStain(stainOff, out stainOff)); + design.DesignData.SetItem(EquipSlot.MainHand, main); + design.DesignData.SetItem(EquipSlot.OffHand, off); + design.DesignData.SetStain(EquipSlot.MainHand, stain); + design.DesignData.SetStain(EquipSlot.OffHand, stainOff); + design.SetApplyEquip(EquipSlot.MainHand, apply); + design.SetApplyEquip(EquipSlot.OffHand, applyOff); + design.SetApplyStain(EquipSlot.MainHand, applyStain); + design.SetApplyStain(EquipSlot.OffHand, applyStainOff); + } + var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse); + design.SetApplyHatVisible(metaValue.Enabled); + design.DesignData.SetHatVisible(metaValue.ForcedValue); + + metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse); + design.SetApplyWeaponVisible(metaValue.Enabled); + design.DesignData.SetWeaponVisible(metaValue.ForcedValue); + + metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); + design.SetApplyVisorToggle(metaValue.Enabled); + design.DesignData.SetVisor(metaValue.ForcedValue); + } + + protected static void LoadCustomize(CustomizationService customizations, JToken? json, DesignBase design, string name) + { + if (json == null) + { + design.DesignData.ModelId = 0; + design.DesignData.Customize = Customize.Default; + Glamourer.Chat.NotificationMessage("The loaded design does not contain any customization data, reset to default.", "Warning", + NotificationType.Warning); + return; + } + + void PrintWarning(string msg) + { + if (msg.Length > 0) + Glamourer.Chat.NotificationMessage($"{msg} ({name})", "Warning", NotificationType.Warning); + } + + design.DesignData.ModelId = json["ModelId"]?.ToObject() ?? 0; + PrintWarning(customizations.ValidateModelId(design.DesignData.ModelId, out design.DesignData.ModelId)); + + var race = (Race)(json[CustomizeIndex.Race.ToString()]?["Value"]?.ToObject() ?? 0); + var clan = (SubRace)(json[CustomizeIndex.Clan.ToString()]?["Value"]?.ToObject() ?? 0); + PrintWarning(customizations.ValidateClan(clan, race, out race, out clan)); + var gender = (Gender)((json[CustomizeIndex.Gender.ToString()]?["Value"]?.ToObject() ?? 0) + 1); + PrintWarning(customizations.ValidateGender(race, gender, out gender)); + design.DesignData.Customize.Race = race; + design.DesignData.Customize.Clan = clan; + design.DesignData.Customize.Gender = gender; + design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject() ?? false); + design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject() ?? false); + design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject() ?? false); + + var set = customizations.AwaitedService.GetList(clan, gender); + + foreach (var idx in Enum.GetValues().Where(set.IsAvailable)) + { + var tok = json[idx.ToString()]; + var data = (CustomizeValue)(tok?["Value"]?.ToObject() ?? 0); + PrintWarning(CustomizationService.ValidateCustomizeValue(set, design.DesignData.Customize.Face, idx, data, out data)); + var apply = tok?["Apply"]?.ToObject() ?? false; + design.DesignData.Customize[idx] = data; + design.SetApplyCustomize(idx, apply); + } + + var wetness = QuadBool.FromJObject(json["Wetness"], "Value", "Apply", QuadBool.NullFalse); + design.DesignData.SetIsWet(wetness.ForcedValue); + design.SetApplyWetness(wetness.Enabled); + } + + public void MigrateBase64(ItemManager items, string base64) + { + DesignData = DesignBase64Migration.MigrateBase64(items, base64, out var equipFlags, out var customizeFlags, out var writeProtected, + out var applyHat, out var applyVisor, out var applyWeapon); + ApplyEquip = equipFlags; + ApplyCustomize = customizeFlags; + SetWriteProtected(writeProtected); + SetApplyHatVisible(applyHat); + SetApplyVisorToggle(applyVisor); + SetApplyWeaponVisible(applyWeapon); + SetApplyWetness(DesignData.IsWet()); + } + + #endregion +} diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs new file mode 100644 index 0000000..218e6a7 --- /dev/null +++ b/Glamourer/Designs/DesignConverter.cs @@ -0,0 +1,121 @@ +using System; +using System.Diagnostics; +using System.Text; +using Glamourer.Customization; +using Glamourer.Services; +using Glamourer.State; +using Glamourer.Structs; +using Glamourer.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs; + +public class DesignConverter +{ + public const byte Version = 3; + + private readonly ItemManager _items; + private readonly DesignManager _designs; + private readonly CustomizationService _customize; + + public DesignConverter(ItemManager items, DesignManager designs, CustomizationService customize) + { + _items = items; + _designs = designs; + _customize = customize; + } + + public JObject ShareJObject(DesignBase design) + => design.JsonSerialize(); + + public JObject ShareJObject(Design design) + => design.JsonSerialize(); + + public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags) + { + var design = Convert(state, equipFlags, customizeFlags); + return ShareJObject(design); + } + + public string ShareBase64(DesignBase design) + => ShareBase64(ShareJObject(design)); + + public string ShareBase64(ActorState state) + => ShareBase64(ShareJObject(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All)); + + public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags) + { + var design = _designs.CreateTemporary(); + design.ApplyEquip = equipFlags & EquipFlagExtensions.All; + design.ApplyCustomize = customizeFlags & CustomizeFlagExtensions.All; + design.SetApplyHatVisible(design.DoApplyEquip(EquipSlot.Head)); + design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head)); + design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand)); + design.SetApplyWetness(design.DesignData.IsWet()); + design.DesignData = state.ModelData; + return design; + } + + public DesignBase? FromBase64(string base64, bool customize, bool equip) + { + var bytes = System.Convert.FromBase64String(base64); + + DesignBase ret; + try + { + switch (bytes[0]) + { + case (byte)'{': + var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes)); + ret = jObj1["Identifier"] != null + ? Design.LoadDesign(_customize, _items, jObj1) + : DesignBase.LoadDesignBase(_customize, _items, jObj1); + break; + case 1: + case 2: + ret = _designs.CreateTemporary(); + ret.MigrateBase64(_items, base64); + break; + case Version: + var version = bytes.DecompressToString(out var decompressed); + var jObj2 = JObject.Parse(decompressed); + Debug.Assert(version == Version); + ret = jObj2["Identifier"] != null + ? Design.LoadDesign(_customize, _items, jObj2) + : DesignBase.LoadDesignBase(_customize, _items, jObj2); + break; + default: throw new Exception($"Unknown Version {bytes[0]}."); + } + } + catch (Exception ex) + { + Glamourer.Log.Error($"[DesignConverter] Could not parse base64 string [{base64}]:\n{ex}"); + return null; + } + + if (!customize) + { + ret.ApplyCustomize = 0; + ret.SetApplyWetness(false); + } + + if (!equip) + { + ret.ApplyEquip = 0; + ret.SetApplyHatVisible(false); + ret.SetApplyWeaponVisible(false); + ret.SetApplyVisorToggle(false); + } + + return ret; + } + + private static string ShareBase64(JObject jObj) + { + var json = jObj.ToString(Formatting.None); + var compressed = json.Compress(Version); + return System.Convert.ToBase64String(compressed); + } +} diff --git a/Glamourer/Designs/DesignFileSystem.cs b/Glamourer/Designs/DesignFileSystem.cs index d0eb79d..3863100 100644 --- a/Glamourer/Designs/DesignFileSystem.cs +++ b/Glamourer/Designs/DesignFileSystem.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Glamourer.Events; +using Glamourer.Interop.Penumbra; using Glamourer.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -101,26 +102,22 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable switch (type) { case DesignChanged.Type.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; + CreateDuplicateLeaf(Root, design.Name.Text, design); + return; case DesignChanged.Type.Deleted: - if (FindLeaf(design, out var leaf)) - Delete(leaf); - break; + if (FindLeaf(design, out var leaf1)) + Delete(leaf1); + return; case DesignChanged.Type.ReloadedAll: Reload(); - break; + return; case DesignChanged.Type.Renamed when data is string oldName: + if (!FindLeaf(design, out var leaf2)) + return; var old = oldName.FixName(); - if (Find(old, out var child) && child is not Folder) - Rename(child, design.Name); - break; + if (old == leaf2.Name || leaf2.Name.IsDuplicateName(out var baseName, out _) && baseName == old) + RenameWithDuplicates(leaf2, design.Name); + return; } } diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index 62ed43f..a5a3245 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -5,7 +5,9 @@ using System.Linq; using Dalamud.Utility; using Glamourer.Customization; using Glamourer.Events; +using Glamourer.Interop.Penumbra; using Glamourer.Services; +using Glamourer.State; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; @@ -78,16 +80,20 @@ public class DesignManager _event.Invoke(DesignChanged.Type.ReloadedAll, null!); } + /// Create a new temporary design without adding it to the manager. + public DesignBase CreateTemporary() + => new(_items); + /// Create a new design of a given name. - public Design Create(string name) + public Design CreateEmpty(string name) { var design = new Design(_items) { CreationDate = DateTimeOffset.UtcNow, LastEdit = DateTimeOffset.UtcNow, Identifier = CreateNewGuid(), - Index = _designs.Count, Name = name, + Index = _designs.Count, }; _designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier}."); @@ -96,6 +102,43 @@ public class DesignManager return design; } + /// Create a new design cloning a given temporary design. + public Design CreateClone(DesignBase clone, string name) + { + var design = new Design(clone) + { + CreationDate = DateTimeOffset.UtcNow, + LastEdit = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Name = name, + Index = _designs.Count, + }; + _designs.Add(design); + Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design."); + _saveService.ImmediateSave(design); + _event.Invoke(DesignChanged.Type.Created, design); + return design; + } + + /// Create a new design cloning a given design. + public Design CreateClone(Design clone, string name) + { + var design = new Design(clone) + { + CreationDate = DateTimeOffset.UtcNow, + LastEdit = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Name = name, + Index = _designs.Count, + }; + _designs.Add(design); + Glamourer.Log.Debug( + $"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}."); + _saveService.ImmediateSave(design); + _event.Invoke(DesignChanged.Type.Created, design); + return design; + } + /// Delete a design. public void Delete(Design design) { @@ -181,6 +224,41 @@ public class DesignManager _event.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx)); } + /// Add an associated mod to a design. + public void AddMod(Design design, Mod mod, ModSettings settings) + { + if (!design.AssociatedMods.TryAdd(mod, settings)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}."); + _event.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings)); + } + + /// Remove an associated mod from a design. + public void RemoveMod(Design design, Mod mod) + { + if (!design.AssociatedMods.Remove(mod, out var settings)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}."); + _event.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings)); + } + + /// Set the write protection status of a design. + public void SetWriteProtection(Design design, bool value) + { + if (!design.SetWriteProtected(value)) + return; + + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected."); + _event.Invoke(DesignChanged.Type.WriteProtection, design, value); + } + /// Change a customization value. public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value) { @@ -210,6 +288,7 @@ public class DesignManager break; } + design.LastEdit = DateTimeOffset.UtcNow; Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}."); _saveService.QueueSave(design); _event.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx)); @@ -237,6 +316,7 @@ public class DesignManager if (!design.DesignData.SetItem(slot, item)) return; + design.LastEdit = DateTimeOffset.UtcNow; Glamourer.Log.Debug( $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id})."); _saveService.QueueSave(design); @@ -257,7 +337,6 @@ public class DesignManager if (item.Type != currentMain.Type) { - var newOffId = FullEquipTypeExtensions.OffhandTypes.Contains(item.Type) ? item.Id : ItemManager.NothingId(item.Type.Offhand()); @@ -332,6 +411,87 @@ public class DesignManager _event.Invoke(DesignChanged.Type.ApplyStain, design, slot); } + /// Change the bool value of one of the meta flags. + public void ChangeMeta(Design design, ActorState.MetaIndex metaIndex, bool value) + { + var change = metaIndex switch + { + ActorState.MetaIndex.Wetness => design.DesignData.SetIsWet(value), + ActorState.MetaIndex.HatState => design.DesignData.SetHatVisible(value), + ActorState.MetaIndex.VisorState => design.DesignData.SetVisor(value), + ActorState.MetaIndex.WeaponState => design.DesignData.SetWeaponVisible(value), + _ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null), + }; + if (!change) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Set value of {metaIndex} to {value}."); + _event.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value)); + } + + /// Change the application value of one of the meta flags. + public void ChangeApplyMeta(Design design, ActorState.MetaIndex metaIndex, bool value) + { + var change = metaIndex switch + { + ActorState.MetaIndex.Wetness => design.SetApplyWetness(value), + ActorState.MetaIndex.HatState => design.SetApplyHatVisible(value), + ActorState.MetaIndex.VisorState => design.SetApplyVisorToggle(value), + ActorState.MetaIndex.WeaponState => design.SetApplyWeaponVisible(value), + _ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null), + }; + if (!change) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}."); + _event.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value)); + } + + /// Apply an entire design based on its appliance rules piece by piece. + public void ApplyDesign(Design design, DesignBase other) + { + if (other.DoApplyEquip(EquipSlot.MainHand)) + ChangeWeapon(design, EquipSlot.MainHand, other.DesignData.Item(EquipSlot.MainHand)); + + if (other.DoApplyEquip(EquipSlot.OffHand)) + ChangeWeapon(design, EquipSlot.OffHand, other.DesignData.Item(EquipSlot.OffHand)); + + if (other.DoApplyStain(EquipSlot.MainHand)) + ChangeStain(design, EquipSlot.MainHand, other.DesignData.Stain(EquipSlot.MainHand)); + + if (other.DoApplyStain(EquipSlot.OffHand)) + ChangeStain(design, EquipSlot.OffHand, other.DesignData.Stain(EquipSlot.OffHand)); + + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + if (other.DoApplyEquip(slot)) + ChangeEquip(design, slot, other.DesignData.Item(slot)); + + if (other.DoApplyStain(slot)) + ChangeStain(design, slot, other.DesignData.Stain(slot)); + } + + foreach (var index in Enum.GetValues()) + { + if (other.DoApplyCustomize(index)) + ChangeCustomize(design, index, other.DesignData.Customize[index]); + } + + if (other.DoApplyHatVisible()) + design.DesignData.SetHatVisible(other.DesignData.IsHatVisible()); + if (other.DoApplyVisorToggle()) + design.DesignData.SetVisor(other.DesignData.IsVisorToggled()); + if (other.DoApplyWeaponVisible()) + design.DesignData.SetWeaponVisible(other.DesignData.IsWeaponVisible()); + if (other.DoApplyWetness()) + design.DesignData.SetIsWet(other.DesignData.IsWet()); + } + private void MigrateOldDesigns() { if (!File.Exists(_saveService.FileNames.MigrationDesignFile)) diff --git a/Glamourer/Events/AutomationChanged.cs b/Glamourer/Events/AutomationChanged.cs index fc78ecb..c38658a 100644 --- a/Glamourer/Events/AutomationChanged.cs +++ b/Glamourer/Events/AutomationChanged.cs @@ -56,8 +56,11 @@ public sealed class AutomationChanged : EventWrapper + /// SetSelector = 0, + + /// + AutoDesignApplier, } public AutomationChanged() diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index 27f9e6e..154880a 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -40,6 +40,12 @@ public sealed class DesignChanged : EventWrapper An existing design had an existing tag renamed. Data is the old name of the tag, the new name of the tag, and the index it had before being resorted [(string, string, int)]. ChangedTag, + /// An existing design had a new associated mod added. Data is the Mod and its Settings [(Mod, ModSettings)]. + AddedMod, + + /// An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. + RemovedMod, + /// An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. Customize, @@ -61,7 +67,10 @@ public sealed class DesignChanged : EventWrapper An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. ApplyStain, - /// An existing design changed one of the meta flags. Data is null. + /// An existing design changed its write protection status. Data is the new value [bool]. + WriteProtection, + + /// An existing design changed one of the meta flags. Data is the flag, whether it was about their applying and the new value [(MetaFlag, bool, bool)]. Other, } diff --git a/Glamourer/Events/StateChanged.cs b/Glamourer/Events/StateChanged.cs index ee86283..9e7f3bb 100644 --- a/Glamourer/Events/StateChanged.cs +++ b/Glamourer/Events/StateChanged.cs @@ -40,6 +40,7 @@ public sealed class StateChanged : EventWrapper(); - _services.GetRequiredService(); // call backup service. _services.GetRequiredService(); // initialize ui. _services.GetRequiredService(); // initialize commands. _services.GetRequiredService(); diff --git a/Glamourer/Gui/Colors.cs b/Glamourer/Gui/Colors.cs index e0b6de8..dc2eeaa 100644 --- a/Glamourer/Gui/Colors.cs +++ b/Glamourer/Gui/Colors.cs @@ -16,6 +16,7 @@ public enum ColorId DisabledAutoSet, AutomationActorAvailable, AutomationActorUnavailable, + HeaderButtons, } public static class Colors @@ -36,6 +37,7 @@ public static class Colors ColorId.DisabledAutoSet => (0xFF808080, "Disabled Automation Set", "An automation set that is currently disabled." ), ColorId.AutomationActorAvailable => (0xFFFFFFFF, "Automation Actor Available", "A character associated with the given automated design set is currently visible." ), ColorId.AutomationActorUnavailable => (0xFF808080, "Automation Actor Unavailable", "No character associated with the given automated design set is currently visible." ), + ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the Incognito toggle." ), _ => (0x00000000, string.Empty, string.Empty ), // @formatter:on }; diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs index 3d09e58..46d74d4 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs @@ -121,17 +121,20 @@ public partial class CustomizationDrawer PercentageInputInt(); ImGui.TextUnformatted(_set.Option(CustomizeIndex.LegacyTattoo)); + if (_set.DataByValue(CustomizeIndex.Face, _customize.Face, out _, _customize.Face) < 0) + ImGui.TextUnformatted("(Using Face 1)"); } private void DrawMultiIcons() { - var options = _set.Order[CharaMakeParams.MenuType.IconCheckmark]; - using var _ = ImRaii.Group(); + var options = _set.Order[CharaMakeParams.MenuType.IconCheckmark]; + using var group = ImRaii.Group(); + var face = _set.DataByValue(CustomizeIndex.Face, _customize.Face, out _, _customize.Face) < 0 ? _set.Faces[0].Value : _customize.Face; foreach (var (featureIdx, idx) in options.WithIndex()) { using var id = SetId(featureIdx); var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero; - var feature = _set.Data(featureIdx, 0, _customize.Face); + var feature = _set.Data(featureIdx, 0, face); var icon = featureIdx == CustomizeIndex.LegacyTattoo ? _legacyTattoo ?? _service.AwaitedService.GetIcon(feature.IconId) : _service.AwaitedService.GetIcon(feature.IconId); diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index e83ecf9..f7d5032 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -1,15 +1,22 @@ -using System.Numerics; +using System; +using System.Numerics; +using System.Xml.Linq; using Dalamud.Interface; using Glamourer.Events; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Interop.Structs; +using Glamourer.Services; using Glamourer.State; using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData; using Penumbra.GameData.Actors; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.ActorTab; @@ -19,6 +26,8 @@ public class ActorPanel private readonly StateManager _stateManager; private readonly CustomizationDrawer _customizationDrawer; private readonly EquipmentDrawer _equipmentDrawer; + private readonly HumanModelList _humans; + private readonly IdentifierService _identification; private ActorIdentifier _identifier; private string _actorName = string.Empty; @@ -27,12 +36,14 @@ public class ActorPanel private ActorState? _state; public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer, - EquipmentDrawer equipmentDrawer) + EquipmentDrawer equipmentDrawer, HumanModelList humans, IdentifierService identification) { _selector = selector; _stateManager = stateManager; _customizationDrawer = customizationDrawer; _equipmentDrawer = equipmentDrawer; + _humans = humans; + _identification = identification; } public void Draw() @@ -47,15 +58,16 @@ public class ActorPanel private void DrawHeader() { var frameHeight = ImGui.GetFrameHeightWithSpacing(); - var color = !_identifier.IsValid ? ImGui.GetColorU32(ImGuiCol.Text) : _data.Valid ? ColorId.ActorAvailable.Value() : ColorId.ActorUnavailable.Value(); + var color = !_identifier.IsValid ? ImGui.GetColorU32(ImGuiCol.Text) : + _data.Valid ? ColorId.ActorAvailable.Value() : ColorId.ActorUnavailable.Value(); var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .Push(ImGuiStyleVar.FrameRounding, 0); ImGuiUtil.DrawTextButton($"{_actorName}##playerHeader", new Vector2(-frameHeight, ImGui.GetFrameHeight()), buttonColor, color); ImGui.SameLine(); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - using (var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()) - .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value())) + using (var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.HeaderButtons.Value()) + .Push(ImGuiCol.Border, ColorId.HeaderButtons.Value())) { if (ImGuiUtil.DrawDisabledButton( $"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", @@ -79,13 +91,9 @@ public class ActorPanel return (_selector.IncognitoMode ? _identifier.Incognito(null) : _identifier.ToString(), Actor.Null); } - private unsafe void DrawPanel() + private void DrawHumanPanel() { - using var child = ImRaii.Child("##Panel", -Vector2.One, true); - if (!child || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state)) - return; - - if (_customizationDrawer.Draw(_state.ModelData.Customize, false)) + if (_customizationDrawer.Draw(_state!.ModelData.Customize, false)) _stateManager.ChangeCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, StateChanged.Source.Manual); foreach (var slot in EquipSlotExtensions.EqdpSlots) @@ -122,6 +130,79 @@ public class ActorPanel } } + private void DrawMonsterPanel() + { + var names = _identification.AwaitedService.ModelCharaNames(_state!.ModelData.ModelId); + var turnHuman = ImGui.Button("Turn Human"); + ImGui.Separator(); + using (var box = ImRaii.ListBox("##MonsterList", + new Vector2(ImGui.GetContentRegionAvail().X, 10 * ImGui.GetTextLineHeightWithSpacing()))) + { + if (names.Count == 0) + ImGui.TextUnformatted("Unknown Monster"); + else + ImGuiClip.ClippedDraw(names, p => ImGui.TextUnformatted($"{p.Name} ({p.Kind.ToName()} #{p.Id})"), + ImGui.GetTextLineHeightWithSpacing()); + } + + ImGui.Separator(); + ImGui.TextUnformatted("Customization Data"); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + { + foreach (var b in _state.ModelData.Customize.Data) + { + using (var g = ImRaii.Group()) + { + ImGui.TextUnformatted($" {b:X2}"); + ImGui.TextUnformatted($"{b,3}"); + } + + ImGui.SameLine(); + if (ImGui.GetContentRegionAvail().X < ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize("XXX").X) + ImGui.NewLine(); + } + + if (ImGui.GetCursorPosX() != 0) + ImGui.NewLine(); + } + + ImGui.Separator(); + ImGui.TextUnformatted("Equipment Data"); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + { + foreach (var b in _state.ModelData.GetEquipmentBytes()) + { + using (var g = ImRaii.Group()) + { + ImGui.TextUnformatted($" {b:X2}"); + ImGui.TextUnformatted($"{b,3}"); + } + + ImGui.SameLine(); + if (ImGui.GetContentRegionAvail().X < ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize("XXX").X) + ImGui.NewLine(); + } + + if (ImGui.GetCursorPosX() != 0) + ImGui.NewLine(); + } + + if (turnHuman) + _stateManager.TurnHuman(_state, StateChanged.Source.Manual); + } + + private unsafe void DrawPanel() + { + using var child = ImRaii.Child("##Panel", -Vector2.One, true); + if (!child || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state)) + return; + + if (_humans.IsHuman(_state.ModelData.ModelId)) + DrawHumanPanel(); + else + DrawMonsterPanel(); + } + private unsafe void RevertButton() { diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs index 67059ba..8c3b2e7 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs @@ -99,9 +99,10 @@ public class ActorSelector _identifier = _objects.Player.GetIdentifier(_actors.AwaitedService); ImGui.SameLine(); - Actor targetActor = _targets.Target?.Address; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth, - "Select the current target, if it is in the list.", _objects.IsInGPose || !targetActor, true)) - _identifier = targetActor.GetIdentifier(_actors.AwaitedService); + var (id, data) = _objects.TargetData; + var tt = data.Valid ? $"Select the current target {id} in the list." : + id.IsValid ? $"The target {id} is not in the list." : "No target selected."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth, tt, _objects.IsInGPose || !data.Valid, true)) + _identifier = id; } } diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index 453ed25..eed59af 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -68,8 +68,8 @@ public class SetPanel ImGuiUtil.DrawTextButton(_selector.SelectionName, new Vector2(-frameHeight, ImGui.GetFrameHeight()), buttonColor); ImGui.SameLine(); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()) - .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HeaderButtons.Value()) + .Push(ImGuiCol.Border, ColorId.HeaderButtons.Value()); if (ImGuiUtil.DrawDisabledButton( $"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", new Vector2(frameHeight, ImGui.GetFrameHeight()), string.Empty, false, true)) diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs index 3d04de0..cce8b83 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs @@ -46,12 +46,12 @@ public class SetSelector : IDisposable _config = config; _actors = actors; _objects = objects; - _event.Subscribe(OnAutomationChanged, AutomationChanged.Priority.SetSelector); + _event.Subscribe(OnAutomationChange, AutomationChanged.Priority.SetSelector); } public void Dispose() { - _event.Unsubscribe(OnAutomationChanged); + _event.Unsubscribe(OnAutomationChange); } public string SelectionName @@ -60,7 +60,7 @@ public class SetSelector : IDisposable public string GetSetName(AutoDesignSet? set, int index) => set == null ? "No Selection" : IncognitoMode ? $"Auto Design Set #{index + 1}" : set.Name; - private void OnAutomationChanged(AutomationChanged.Type type, AutoDesignSet? set, object? data) + private void OnAutomationChange(AutomationChanged.Type type, AutoDesignSet? set, object? data) { switch (type) { diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index cdb275c..19dc4c3 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; +using System.Text; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; @@ -19,7 +20,9 @@ using Glamourer.Interop.Structs; using Glamourer.Services; using Glamourer.State; using Glamourer.Unlocks; +using Glamourer.Utility; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; @@ -42,7 +45,7 @@ public unsafe class DebugTab : ITab private readonly ObjectTable _objects; private readonly ObjectManager _objectManager; private readonly GlamourerIpc _ipc; - private readonly CodeService _code; + private readonly CodeService _code; private readonly ItemManager _items; private readonly ActorService _actors; @@ -54,6 +57,7 @@ public unsafe class DebugTab : ITab private readonly DesignManager _designManager; private readonly DesignFileSystem _designFileSystem; private readonly AutoDesignManager _autoDesignManager; + private readonly DesignConverter _designConverter; private readonly PenumbraChangedItemTooltip _penumbraTooltip; @@ -70,7 +74,7 @@ public unsafe class DebugTab : ITab DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config, PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface, AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks, - ItemUnlockManager itemUnlocks) + ItemUnlockManager itemUnlocks, DesignConverter designConverter) { _changeCustomizeService = changeCustomizeService; _visorService = visorService; @@ -92,9 +96,10 @@ public unsafe class DebugTab : ITab _pluginInterface = pluginInterface; _autoDesignManager = autoDesignManager; _jobs = jobs; - _code = code; + _code = code; _customizeUnlocks = customizeUnlocks; _itemUnlocks = itemUnlocks; + _designConverter = designConverter; } public ReadOnlySpan Label @@ -817,6 +822,7 @@ public unsafe class DebugTab : ITab DrawDesignManager(); DrawDesignTester(); + DrawDesignConverter(); } private void DrawDesignManager() @@ -927,6 +933,83 @@ public unsafe class DebugTab : ITab } } + private string _clipboardText = string.Empty; + private byte[] _clipboardData = Array.Empty(); + private byte[] _dataUncompressed = Array.Empty(); + private byte _version = 0; + private string _textUncompressed = string.Empty; + private JObject? _json = null; + private DesignBase? _tmpDesign = null; + private Exception? _clipboardProblem = null; + + private void DrawDesignConverter() + { + using var tree = ImRaii.TreeNode("Design Converter"); + if (!tree) + return; + + if (ImGui.Button("Import Clipboard")) + { + _clipboardText = string.Empty; + _clipboardData = Array.Empty(); + _dataUncompressed = Array.Empty(); + _textUncompressed = string.Empty; + _json = null; + _tmpDesign = null; + _clipboardProblem = null; + + try + { + _clipboardText = ImGui.GetClipboardText(); + _clipboardData = Convert.FromBase64String(_clipboardText); + _version = _clipboardData.Decompress(out _dataUncompressed); + _textUncompressed = Encoding.UTF8.GetString(_dataUncompressed); + _json = JObject.Parse(_textUncompressed); + _tmpDesign = _designConverter.FromBase64(_clipboardText, true, true); + } + catch (Exception ex) + { + _clipboardProblem = ex; + } + } + + if (_clipboardText.Length > 0) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(_clipboardText); + } + + if (_clipboardData.Length > 0) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(string.Join(" ", _clipboardData.Select(b => b.ToString("X2")))); + } + + if (_dataUncompressed.Length > 0) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(string.Join(" ", _dataUncompressed.Select(b => b.ToString("X2")))); + } + + if (_textUncompressed.Length > 0) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(_textUncompressed); + } + + if (_json != null) + ImGui.TextUnformatted("JSON Parsing Successful!"); + + if (_tmpDesign != null) + DrawDesign(_tmpDesign); + + if (_clipboardProblem != null) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(_clipboardProblem.ToString()); + } + } + public void DrawState(ActorData data, ActorState state) { using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); @@ -955,20 +1038,20 @@ public unsafe class DebugTab : ITab return $"{item.Name} ({item.ModelId.Value}{(item.WeaponType != 0 ? $"-{item.WeaponType.Value}" : string.Empty)}-{item.Variant})"; } - PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaFlag.ModelId]); + PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaIndex.ModelId]); ImGui.TableNextRow(); - PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaFlag.Wetness]); + PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaIndex.Wetness]); ImGui.TableNextRow(); if (state.BaseData.ModelId == 0 && state.ModelData.ModelId == 0) { - PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaFlag.HatState]); + PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaIndex.HatState]); ImGui.TableNextRow(); PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(), - state[ActorState.MetaFlag.VisorState]); + state[ActorState.MetaIndex.VisorState]); ImGui.TableNextRow(); PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(), - state[ActorState.MetaFlag.WeaponState]); + state[ActorState.MetaIndex.WeaponState]); ImGui.TableNextRow(); foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) { @@ -1053,33 +1136,36 @@ public unsafe class DebugTab : ITab } } - private void DrawDesign(Design design) + private void DrawDesign(DesignBase design) { using var table = ImRaii.Table("##equip", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); - ImGuiUtil.DrawTableColumn("Name"); - ImGuiUtil.DrawTableColumn(design.Name); - ImGuiUtil.DrawTableColumn($"({design.Index})"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted("Description (Hover)"); - ImGuiUtil.HoverTooltip(design.Description); - ImGui.TableNextRow(); + if (design is Design d) + { + ImGuiUtil.DrawTableColumn("Name"); + ImGuiUtil.DrawTableColumn(d.Name); + ImGuiUtil.DrawTableColumn($"({d.Index})"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Description (Hover)"); + ImGuiUtil.HoverTooltip(d.Description); + ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Identifier"); - ImGuiUtil.DrawTableColumn(design.Identifier.ToString()); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Design File System Path"); - ImGuiUtil.DrawTableColumn(_designFileSystem.FindLeaf(design, out var leaf) ? leaf.FullName() : "No Path Known"); - ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Identifier"); + ImGuiUtil.DrawTableColumn(d.Identifier.ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Design File System Path"); + ImGuiUtil.DrawTableColumn(_designFileSystem.FindLeaf(d, out var leaf) ? leaf.FullName() : "No Path Known"); + ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Creation"); - ImGuiUtil.DrawTableColumn(design.CreationDate.ToString()); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Update"); - ImGuiUtil.DrawTableColumn(design.LastEdit.ToString()); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Tags"); - ImGuiUtil.DrawTableColumn(string.Join(", ", design.Tags)); - ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Creation"); + ImGuiUtil.DrawTableColumn(d.CreationDate.ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Update"); + ImGuiUtil.DrawTableColumn(d.LastEdit.ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Tags"); + ImGuiUtil.DrawTableColumn(string.Join(", ", d.Tags)); + ImGui.TableNextRow(); + } foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) { @@ -1174,10 +1260,8 @@ public unsafe class DebugTab : ITab foreach (var (identifier, state) in _state.Where(kvp => !_objectManager.ContainsKey(kvp.Key))) { using var t = ImRaii.TreeNode(identifier.ToString()); - if (!t) - return; - - DrawState(ActorData.Invalid, state); + if (t) + DrawState(ActorData.Invalid, state); } } diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs new file mode 100644 index 0000000..68471d6 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs @@ -0,0 +1,173 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Glamourer.Designs; +using Glamourer.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public class DesignDetailTab +{ + private readonly SaveService _saveService; + private readonly DesignFileSystemSelector _selector; + private readonly DesignFileSystem _fileSystem; + private readonly DesignManager _manager; + private readonly TagButtons _tagButtons = new(); + + private string? _newPath; + private string? _newDescription; + private string? _newName; + + private bool _editDescriptionMode; + + public DesignDetailTab(SaveService saveService, DesignFileSystemSelector selector, DesignManager manager, DesignFileSystem fileSystem) + { + _saveService = saveService; + _selector = selector; + _manager = manager; + _fileSystem = fileSystem; + } + + public void Draw() + { + if (!ImGui.CollapsingHeader("Design Details")) + return; + + DrawDesignInfoTable(); + DrawDescription(); + ImGui.NewLine(); + } + + + private void DrawDesignInfoTable() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + using var table = ImRaii.Table("Details", 2); + if (!table) + return; + + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X); + ImGui.TableSetupColumn("Data", ImGuiTableColumnFlags.WidthStretch); + + ImGuiUtil.DrawFrameColumn("Design Name"); + ImGui.TableNextColumn(); + var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); + var name = _newName ?? _selector.Selected!.Name; + ImGui.SetNextItemWidth(width.X); + if (ImGui.InputText("##Name", ref name, 128)) + _newName = name; + + if (ImGui.IsItemDeactivatedAfterEdit()) + { + _manager.Rename(_selector.Selected!, name); + _newName = null; + } + + var identifier = _selector.Selected!.Identifier.ToString(); + ImGuiUtil.DrawFrameColumn("Unique Identifier"); + ImGui.TableNextColumn(); + var fileName = _saveService.FileNames.DesignFile(_selector.Selected!); + using (var mono = ImRaii.PushFont(UiBuilder.MonoFont)) + { + if (ImGui.Button(identifier, width)) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception ex) + { + Glamourer.Chat.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}", "Failure", + NotificationType.Warning); + } + } + + ImGuiUtil.HoverTooltip($"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice."); + + ImGuiUtil.DrawFrameColumn("Full Selector Path"); + ImGui.TableNextColumn(); + var path = _newPath ?? _selector.SelectedLeaf!.FullName(); + ImGui.SetNextItemWidth(width.X); + if (ImGui.InputText("##Path", ref path, 1024)) + _newPath = path; + + if (ImGui.IsItemDeactivatedAfterEdit()) + try + { + _fileSystem.RenameAndMove(_selector.SelectedLeaf!, path); + _newPath = null; + } + catch (Exception ex) + { + Glamourer.Chat.NotificationMessage(ex, ex.Message, "Could not rename or move design", "Error", NotificationType.Error); + } + + ImGuiUtil.DrawFrameColumn("Creation Date"); + ImGui.TableNextColumn(); + ImGuiUtil.DrawTextButton(_selector.Selected!.CreationDate.LocalDateTime.ToString("F"), width, 0); + + ImGuiUtil.DrawFrameColumn("Last Update Date"); + ImGui.TableNextColumn(); + ImGuiUtil.DrawTextButton(_selector.Selected!.LastEdit.LocalDateTime.ToString("F"), width, 0); + + ImGuiUtil.DrawFrameColumn("Tags"); + ImGui.TableNextColumn(); + DrawTags(); + } + + private void DrawTags() + { + var idx = _tagButtons.Draw(string.Empty, string.Empty, _selector.Selected!.Tags, out var editedTag); + if (idx < 0) + return; + + if (idx < _selector.Selected!.Tags.Length) + { + if (editedTag.Length == 0) + _manager.RemoveTag(_selector.Selected!, idx); + else + _manager.RenameTag(_selector.Selected!, idx, editedTag); + } + else + { + _manager.AddTag(_selector.Selected!, editedTag); + } + } + + private void DrawDescription() + { + var desc = _selector.Selected!.Description; + var size = new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeightWithSpacing()); + if (!_editDescriptionMode) + { + using (var textBox = ImRaii.ListBox("##desc", size)) + { + ImGuiUtil.TextWrapped(desc); + } + + if (ImGui.Button("Edit Description")) + _editDescriptionMode = true; + } + else + { + var edit = _newDescription ?? desc; + if (ImGui.InputTextMultiline("##desc", ref edit, (uint)Math.Max(2000, 4 * edit.Length), size)) + _newDescription = edit; + + if (ImGui.IsItemDeactivatedAfterEdit()) + { + _manager.ChangeDescription(_selector.Selected!, edit); + _newDescription = null; + } + + if (ImGui.Button("Stop Editing")) + _editDescriptionMode = false; + } + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs index fd86675..da13c16 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs @@ -1,6 +1,9 @@ -using System.Numerics; +using System; +using System.Collections.Generic; +using System.Numerics; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; using Glamourer.Designs; using Glamourer.Events; using ImGuiNET; @@ -13,9 +16,14 @@ namespace Glamourer.Gui.Tabs.DesignTab; public sealed class DesignFileSystemSelector : FileSystemSelector { - private readonly DesignManager _designManager; - private readonly DesignChanged _event; - private readonly Configuration _config; + private readonly DesignManager _designManager; + private readonly DesignChanged _event; + private readonly Configuration _config; + private readonly DesignConverter _converter; + + private string? _clipboardText; + private Design? _cloneDesign = null; + private string _newName = string.Empty; public bool IncognitoMode { @@ -27,24 +35,37 @@ public sealed class DesignFileSystemSelector : FileSystemSelector base.SelectedLeaf; + public struct DesignState { } public DesignFileSystemSelector(DesignManager designManager, DesignFileSystem fileSystem, KeyState keyState, DesignChanged @event, - Configuration config) + Configuration config, DesignConverter converter) : base(fileSystem, keyState) { _designManager = designManager; _event = @event; _config = config; + _converter = converter; _event.Subscribe(OnDesignChange, DesignChanged.Priority.DesignFileSystemSelector); - AddButton(DeleteButton, 1000); + + AddButton(NewDesignButton, 0); + AddButton(ImportDesignButton, 10); + AddButton(CloneDesignButton, 20); + AddButton(DeleteButton, 1000); + } + + protected override void DrawPopups() + { + DrawNewDesignPopup(); } protected override void DrawLeafName(FileSystem.Leaf leaf, in DesignState state, bool selected) { var flag = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - var name = IncognitoMode ? leaf.Value.Incognito : leaf.Name; + var name = IncognitoMode ? leaf.Value.Incognito : leaf.Value.Name.Text; using var _ = ImRaii.TreeNode(name, flag); } @@ -78,11 +99,51 @@ public sealed class DesignFileSystemSelector : FileSystemSelector 0) + ImGui.SetTooltip(hoverText); } private string SelectionName => _selector.Selected == null ? "No Selection" : _selector.IncognitoMode ? _selector.Selected.Incognito : _selector.Selected.Name.Text; + private void DrawMetaData() + { + if (!ImGui.CollapsingHeader("MetaData")) + return; + + using (var group1 = ImRaii.Group()) + { + var apply = _selector.Selected!.DesignData.IsHatVisible(); + if (ImGui.Checkbox("Hat Visible", ref apply)) + _manager.ChangeMeta(_selector.Selected, ActorState.MetaIndex.HatState, apply); + + apply = _selector.Selected.DesignData.IsWeaponVisible(); + if (ImGui.Checkbox("Weapon Visible", ref apply)) + _manager.ChangeMeta(_selector.Selected, ActorState.MetaIndex.WeaponState, apply); + } + + ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2); + + using (var group2 = ImRaii.Group()) + { + var apply = _selector.Selected.DesignData.IsVisorToggled(); + if (ImGui.Checkbox("Visor Toggled", ref apply)) + _manager.ChangeMeta(_selector.Selected, ActorState.MetaIndex.VisorState, apply); + + apply = _selector.Selected.DesignData.IsWet(); + if (ImGui.Checkbox("Force Wetness", ref apply)) + _manager.ChangeMeta(_selector.Selected, ActorState.MetaIndex.Wetness, apply); + } + } + + private void DrawEquipment() + { + if (!ImGui.CollapsingHeader("Equipment")) + return; + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var stain = _selector.Selected!.DesignData.Stain(slot); + if (_equipmentDrawer.DrawStain(stain, slot, out var newStain)) + _manager.ChangeStain(_selector.Selected!, slot, newStain.RowIndex); + + ImGui.SameLine(); + var armor = _selector.Selected!.DesignData.Item(slot); + if (_equipmentDrawer.DrawArmor(armor, slot, out var newArmor, _selector.Selected!.DesignData.Customize.Gender, + _selector.Selected!.DesignData.Customize.Race)) + _manager.ChangeEquip(_selector.Selected!, slot, newArmor); + } + + var mhStain = _selector.Selected!.DesignData.Stain(EquipSlot.MainHand); + if (_equipmentDrawer.DrawStain(mhStain, EquipSlot.MainHand, out var newMhStain)) + _manager.ChangeStain(_selector.Selected!, EquipSlot.MainHand, newMhStain.RowIndex); + + ImGui.SameLine(); + var mh = _selector.Selected!.DesignData.Item(EquipSlot.MainHand); + if (_equipmentDrawer.DrawMainhand(mh, true, out var newMh)) + _manager.ChangeWeapon(_selector.Selected!, EquipSlot.MainHand, newMh); + + if (newMh.Type.Offhand() is not FullEquipType.Unknown) + { + var ohStain = _selector.Selected!.DesignData.Stain(EquipSlot.OffHand); + if (_equipmentDrawer.DrawStain(ohStain, EquipSlot.OffHand, out var newOhStain)) + _manager.ChangeStain(_selector.Selected!, EquipSlot.OffHand, newOhStain.RowIndex); + + ImGui.SameLine(); + var oh = _selector.Selected!.DesignData.Item(EquipSlot.OffHand); + if (_equipmentDrawer.DrawMainhand(oh, false, out var newOh)) + _manager.ChangeWeapon(_selector.Selected!, EquipSlot.OffHand, newOh); + } + } + + private void DrawCustomize() + { + if (ImGui.CollapsingHeader("Customization")) + _customizationDrawer.Draw(_selector.Selected!.DesignData.Customize, _selector.Selected!.WriteProtected()); + } + + private void DrawApplicationRules() + { + if (!ImGui.CollapsingHeader("Application Rules")) + return; + + using (var group1 = ImRaii.Group()) + { + var set = _customizationService.AwaitedService.GetList(_selector.Selected!.DesignData.Customize.Clan, + _selector.Selected!.DesignData.Customize.Gender); + var all = CustomizationExtensions.All.Where(set.IsAvailable).Select(c => c.ToFlag()).Aggregate((a, b) => a | b); + var flags = (_selector.Selected!.ApplyCustomize & all) == 0 ? 0 : (_selector.Selected!.ApplyCustomize & all) == all ? 3 : 1; + if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3)) + { + var newFlags = flags == 3; + foreach (var index in CustomizationExtensions.All) + _manager.ChangeApplyCustomize(_selector.Selected!, index, newFlags); + } + + var applyClan = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Clan); + if (ImGui.Checkbox($"Apply {CustomizeIndex.Clan.ToDefaultName()}", ref applyClan)) + _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, applyClan); + + var applyGender = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Gender); + if (ImGui.Checkbox($"Apply {CustomizeIndex.Gender.ToDefaultName()}", ref applyGender)) + _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, applyGender); + + + foreach (var index in CustomizationExtensions.All.Where(set.IsAvailable)) + { + var apply = _selector.Selected!.DoApplyCustomize(index); + if (ImGui.Checkbox($"Apply {index.ToDefaultName()}", ref apply)) + _manager.ChangeApplyCustomize(_selector.Selected!, index, apply); + } + } + + ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2); + using (var group2 = ImRaii.Group()) + { + void ApplyEquip(string label, EquipFlag all, bool stain, IEnumerable slots) + { + var flags = (uint)(all & _selector.Selected!.ApplyEquip); + + var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)all); + if (stain) + foreach (var slot in slots) + { + var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToStainFlag()) : _selector.Selected!.DoApplyStain(slot); + if (ImGui.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange) + _manager.ChangeApplyStain(_selector.Selected!, slot, apply); + } + else + foreach (var slot in slots) + { + var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToFlag()) : _selector.Selected!.DoApplyEquip(slot); + if (ImGui.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange) + _manager.ChangeApplyEquip(_selector.Selected!, slot, apply); + } + } + + ApplyEquip("Weapons", AutoDesign.WeaponFlags, false, new[] + { + EquipSlot.MainHand, + EquipSlot.OffHand, + }); + + ImGui.NewLine(); + ApplyEquip("Armor", AutoDesign.ArmorFlags, false, EquipSlotExtensions.EquipmentSlots); + + ImGui.NewLine(); + ApplyEquip("Accessories", AutoDesign.AccessoryFlags, false, EquipSlotExtensions.AccessorySlots); + + ImGui.NewLine(); + ApplyEquip("Dyes", AutoDesign.StainFlags, true, + EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.MainHand).Prepend(EquipSlot.OffHand)); + + ImGui.NewLine(); + const uint all = 0x0Fu; + var flags = (_selector.Selected!.DoApplyHatVisible() ? 0x01u : 0x00) + | (_selector.Selected!.DoApplyVisorToggle() ? 0x02u : 0x00) + | (_selector.Selected!.DoApplyWeaponVisible() ? 0x04u : 0x00) + | (_selector.Selected!.DoApplyWetness() ? 0x08u : 0x00); + var bigChange = ImGui.CheckboxFlags("Apply All Meta Changes", ref flags, all); + var apply = bigChange ? (flags & 0x01) == 0x01 : _selector.Selected!.DoApplyHatVisible(); + if (ImGui.Checkbox("Apply Hat Visibility", ref apply) || bigChange) + _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.HatState, apply); + + apply = bigChange ? (flags & 0x02) == 0x02 : _selector.Selected!.DoApplyVisorToggle(); + if (ImGui.Checkbox("Apply Visor State", ref apply) || bigChange) + _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.VisorState, apply); + + apply = bigChange ? (flags & 0x04) == 0x04 : _selector.Selected!.DoApplyWeaponVisible(); + if (ImGui.Checkbox("Apply Weapon Visibility", ref apply) || bigChange) + _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.WeaponState, apply); + + apply = bigChange ? (flags & 0x08) == 0x08 : _selector.Selected!.DoApplyWetness(); + if (ImGui.Checkbox("Apply Wetness", ref apply) || bigChange) + _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.Wetness, apply); + } + } + public void Draw() { - using var group = ImRaii.Group(); + using var group = ImRaii.Group(); DrawHeader(); - + var design = _selector.Selected; using var child = ImRaii.Child("##Panel", -Vector2.One, true); if (!child || design == null) return; - if (ImGui.Button("TEST")) - { - var (id, data) = _objects.PlayerData; - - if (data.Valid && _state.GetOrCreate(id, data.Objects[0], out var state)) - _state.ApplyDesign(design, state); - } - - _customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected()); - - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - var stain = design.DesignData.Stain(slot); - if (_equipmentDrawer.DrawStain(stain, slot, out var newStain)) - _manager.ChangeStain(design, slot, newStain.RowIndex); - - ImGui.SameLine(); - var armor = design.DesignData.Item(slot); - if (_equipmentDrawer.DrawArmor(armor, slot, out var newArmor, design.DesignData.Customize.Gender, design.DesignData.Customize.Race)) - _manager.ChangeEquip(design, slot, newArmor); - } - - var mhStain = design.DesignData.Stain(EquipSlot.MainHand); - if (_equipmentDrawer.DrawStain(mhStain, EquipSlot.MainHand, out var newMhStain)) - _manager.ChangeStain(design, EquipSlot.MainHand, newMhStain.RowIndex); + DrawButtonRow(); + DrawMetaData(); + DrawCustomize(); + DrawEquipment(); + _designDetails.Draw(); + DrawApplicationRules(); + _modAssociations.Draw(); + } + private void DrawButtonRow() + { + DrawSetFromClipboard(); ImGui.SameLine(); - var mh = design.DesignData.Item(EquipSlot.MainHand); - if (_equipmentDrawer.DrawMainhand(mh, true, out var newMh)) - _manager.ChangeWeapon(design, EquipSlot.MainHand, newMh); + DrawExportToClipboard(); + ImGui.SameLine(); + DrawApplyToSelf(); + ImGui.SameLine(); + DrawApplyToTarget(); + } - if (newMh.Type.Offhand() is not FullEquipType.Unknown) + private void DrawSetFromClipboard() + { + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Try to apply a design from your clipboard.", _selector.Selected!.WriteProtected(), true)) + return; + + try { - var ohStain = design.DesignData.Stain(EquipSlot.OffHand); - if (_equipmentDrawer.DrawStain(ohStain, EquipSlot.OffHand, out var newOhStain)) - _manager.ChangeStain(design, EquipSlot.OffHand, newOhStain.RowIndex); - - ImGui.SameLine(); - var oh = design.DesignData.Item(EquipSlot.OffHand); - if (_equipmentDrawer.DrawMainhand(oh, false, out var newOh)) - _manager.ChangeWeapon(design, EquipSlot.OffHand, newOh); + var text = ImGui.GetClipboardText(); + var design = _converter.FromBase64(text, true, true) ?? throw new Exception("The clipboard did not contain valid data."); + _manager.ApplyDesign(_selector.Selected!, design); + } + catch (Exception ex) + { + Glamourer.Chat.NotificationMessage(ex, $"Could not apply clipboard to {_selector.Selected!.Name}.", + $"Could not apply clipboard to design {_selector.Selected!.Identifier}", "Failure", NotificationType.Error); } } + + private void DrawExportToClipboard() + { + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Copy.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Copy the current design to your clipboard.", false, true)) + return; + + try + { + var text = _converter.ShareBase64(_selector.Selected!); + ImGui.SetClipboardText(text); + } + catch (Exception ex) + { + Glamourer.Chat.NotificationMessage(ex, $"Could not copy {_selector.Selected!.Name} data to clipboard.", + $"Could not copy data from design {_selector.Selected!.Identifier} to clipboard", "Failure", NotificationType.Error); + } + } + + private void DrawApplyToSelf() + { + var (id, data) = _objects.PlayerData; + if (!ImGuiUtil.DrawDisabledButton("Apply to Yourself", Vector2.Zero, "Apply the current design with its settings to your character.", + !data.Valid)) + return; + + if (_state.GetOrCreate(id, data.Objects[0], out var state)) + _state.ApplyDesign(_selector.Selected!, state); + } + + private void DrawApplyToTarget() + { + var (id, data) = _objects.TargetData; + var tt = id.IsValid + ? data.Valid + ? "Apply the current design with its settings to your current target." + : "The current target can not be manipulated." + : "No valid target selected."; + if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt, !data.Valid)) + return; + + if (_state.GetOrCreate(id, data.Objects[0], out var state)) + _state.ApplyDesign(_selector.Selected!, state); + } } diff --git a/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs b/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs new file mode 100644 index 0000000..8aa7e09 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs @@ -0,0 +1,144 @@ +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; +using Glamourer.Designs; +using Glamourer.Interop.Penumbra; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public class ModAssociationsTab +{ + private readonly PenumbraService _penumbra; + private readonly DesignFileSystemSelector _selector; + private readonly DesignManager _manager; + private readonly ModCombo _modCombo; + + public ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelector selector, DesignManager manager) + { + _penumbra = penumbra; + _selector = selector; + _manager = manager; + _modCombo = new ModCombo(penumbra); + } + + public void Draw() + { + if (!ImGui.CollapsingHeader("Mod Associations")) + return; + + DrawApplyAllButton(); + DrawTable(); + } + + private void DrawApplyAllButton() + { + var current = _penumbra.CurrentCollection; + if (!ImGuiUtil.DrawDisabledButton($"Try Applying All Associated Mods to {current}##applyAll", + new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, current is "")) + return; + + foreach (var (mod, settings) in _selector.Selected!.AssociatedMods) + _penumbra.SetMod(mod, settings); + } + + private void DrawTable() + { + using var table = ImRaii.Table("Mods", 6, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGui.TableSetupColumn("##Delete", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("Mod Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Directory Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("State").X); + ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Priority").X); + ImGui.TableSetupColumn("##Options", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Try Applyingm").X); + ImGui.TableHeadersRow(); + + Mod? removedMod = null; + foreach (var ((mod, settings), idx) in _selector.Selected!.AssociatedMods.WithIndex()) + { + using var id = ImRaii.PushId(idx); + DrawAssociatedModRow(mod, settings, out removedMod); + } + + DrawNewModRow(); + + if (removedMod.HasValue) + _manager.RemoveMod(_selector.Selected!, removedMod.Value); + } + + private void DrawAssociatedModRow(Mod mod, ModSettings settings, out Mod? removedMod) + { + removedMod = null; + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Delete this mod from associations", false, true)) + removedMod = mod; + + ImGuiUtil.DrawTableColumn(mod.Name); + ImGuiUtil.DrawTableColumn(mod.DirectoryName); + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGuiUtil.DrawTextButton((settings.Enabled ? FontAwesomeIcon.Check : FontAwesomeIcon.Cross).ToIconString(), + new Vector2(ImGui.GetContentRegionAvail().X, 0), 0); + } + + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(settings.Priority.ToString()); + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton("Try Applying", new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, + !_penumbra.Available)) + { + var text = _penumbra.SetMod(mod, settings); + if (text.Length > 0) + Glamourer.Chat.NotificationMessage(text, "Failure", NotificationType.Warning); + } + + DrawAssociatedModTooltip(settings); + } + + private static void DrawAssociatedModTooltip(ModSettings settings) + { + if (settings is not { Enabled: true, Settings.Count: > 0 } || !ImGui.IsItemHovered()) + return; + + using var t = ImRaii.Tooltip(); + ImGui.TextUnformatted("This will also try to apply the following settings to the current collection:"); + + ImGui.NewLine(); + using (var _ = ImRaii.Group()) + { + ModCombo.DrawSettingsLeft(settings); + } + + ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2); + using (var _ = ImRaii.Group()) + { + ModCombo.DrawSettingsRight(settings); + } + } + + private void DrawNewModRow() + { + var currentName = _modCombo.CurrentSelection.Mod.Name; + ImGui.TableNextColumn(); + var tt = currentName.IsNullOrEmpty() + ? "Please select a mod first." + : _selector.Selected!.AssociatedMods.ContainsKey(_modCombo.CurrentSelection.Mod) + ? "The design already contains an association with the selected mod." + : string.Empty; + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), new Vector2(ImGui.GetFrameHeight()), tt, tt.Length > 0, + true)) + _manager.AddMod(_selector.Selected!, _modCombo.CurrentSelection.Mod, _modCombo.CurrentSelection.Settings); + ImGui.TableNextColumn(); + _modCombo.Draw("##new", currentName.IsNullOrEmpty() ? "Select new Mod..." : currentName, string.Empty, + 200 * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeight()); + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs b/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs new file mode 100644 index 0000000..a5bce6e --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs @@ -0,0 +1,85 @@ +using System; +using System.Numerics; +using Dalamud.Interface; +using Glamourer.Interop.Penumbra; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> +{ + public ModCombo(PenumbraService penumbra) + : base(penumbra.GetMods) + { } + + protected override string ToString((Mod Mod, ModSettings Settings) obj) + => obj.Mod.Name; + + protected override bool IsVisible(int globalIndex, LowerString filter) + => filter.IsContained(Items[globalIndex].Mod.Name) || filter.IsContained(Items[globalIndex].Mod.DirectoryName); + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + using var id = ImRaii.PushId(globalIdx); + var (mod, settings) = Items[globalIdx]; + bool ret; + using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !settings.Enabled)) + { + ret = ImGui.Selectable(mod.Name, selected); + } + + if (ImGui.IsItemHovered()) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 2 * ImGuiHelpers.GlobalScale); + using var tt = ImRaii.Tooltip(); + var namesDifferent = mod.Name != mod.DirectoryName; + ImGui.Dummy(new Vector2(300 * ImGuiHelpers.GlobalScale, 0)); + using (var group = ImRaii.Group()) + { + if (namesDifferent) + ImGui.TextUnformatted("Directory Name"); + ImGui.TextUnformatted("Enabled"); + ImGui.TextUnformatted("Priority"); + DrawSettingsLeft(settings); + } + + ImGui.SameLine(Math.Max(ImGui.GetItemRectSize().X + 3 * ImGui.GetStyle().ItemSpacing.X, 150 * ImGuiHelpers.GlobalScale)); + using (var group = ImRaii.Group()) + { + if (namesDifferent) + ImGui.TextUnformatted(mod.DirectoryName); + ImGui.TextUnformatted(settings.Enabled.ToString()); + ImGui.TextUnformatted(settings.Priority.ToString()); + DrawSettingsRight(settings); + } + } + + return ret; + } + + public static void DrawSettingsLeft(ModSettings settings) + { + foreach (var setting in settings.Settings) + { + ImGui.TextUnformatted(setting.Key); + for (var i = 1; i < setting.Value.Count; ++i) + ImGui.NewLine(); + } + } + + public static void DrawSettingsRight(ModSettings settings) + { + foreach (var setting in settings.Settings) + { + if (setting.Value.Count == 0) + ImGui.TextUnformatted(""); + else + foreach (var option in setting.Value) + ImGui.TextUnformatted(option); + } + } +} diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs index afd5a1b..d0386ad 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs @@ -42,7 +42,6 @@ public class UnlockOverview if (ImGui.Selectable(type.ToName(), _selected1 == type)) { - ClearIcons(_selected1); _selected1 = type; _selected2 = SubRace.Unknown; _selected3 = Gender.Unknown; @@ -59,7 +58,6 @@ public class UnlockOverview if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint", _selected2 == clan && _selected3 == gender)) { - ClearIcons(_selected1); _selected1 = FullEquipType.Unknown; _selected2 = clan; _selected3 = gender; @@ -68,15 +66,6 @@ public class UnlockOverview } } - private void ClearIcons(FullEquipType type) - { - if (!_items.ItemService.AwaitedService.TryGetValue(type, out var items)) - return; - - foreach (var item in items) - _customizations.AwaitedService.RemoveIcon(item.IconId); - } - public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureCache textureCache) { diff --git a/Glamourer/Interop/MetaService.cs b/Glamourer/Interop/MetaService.cs index 3fa13c9..a8a36b5 100644 --- a/Glamourer/Interop/MetaService.cs +++ b/Glamourer/Interop/MetaService.cs @@ -38,7 +38,12 @@ public unsafe class MetaService : IDisposable if (!actor.IsCharacter) return; - _hideHatGearHook.Original(&actor.AsCharacter->DrawData, 0, (byte)(value ? 1 : 0)); + // The function seems to not do anything if the head is 0, sometimes? + var old = actor.AsCharacter->DrawData.Head.Id; + if (old == 0) + actor.AsCharacter->DrawData.Head.Id = 1; + _hideHatGearHook.Original(&actor.AsCharacter->DrawData, 0, (byte)(value ? 0 : 1)); + actor.AsCharacter->DrawData.Head.Id = old; } public void SetWeaponState(Actor actor, bool value) diff --git a/Glamourer/Interop/ObjectManager.cs b/Glamourer/Interop/ObjectManager.cs index 94e080d..0a7d0d8 100644 --- a/Glamourer/Interop/ObjectManager.cs +++ b/Glamourer/Interop/ObjectManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; @@ -13,17 +12,19 @@ namespace Glamourer.Interop; public class ObjectManager : IReadOnlyDictionary { - private readonly Framework _framework; - private readonly ClientState _clientState; - private readonly ObjectTable _objects; - private readonly ActorService _actors; + private readonly Framework _framework; + private readonly ClientState _clientState; + private readonly ObjectTable _objects; + private readonly ActorService _actors; + private readonly TargetManager _targets; - public ObjectManager(Framework framework, ClientState clientState, ObjectTable objects, ActorService actors) + public ObjectManager(Framework framework, ClientState clientState, ObjectTable objects, ActorService actors, TargetManager targets) { _framework = framework; _clientState = clientState; _objects = objects; _actors = actors; + _targets = targets; } public DateTime LastUpdate { get; private set; } @@ -31,7 +32,8 @@ public class ObjectManager : IReadOnlyDictionary public bool IsInGPose { get; private set; } public ushort World { get; private set; } - private readonly Dictionary _identifiers = new(200); + private readonly Dictionary _identifiers = new(200); + private readonly Dictionary _allWorldIdentifiers = new(200); public IReadOnlyDictionary Identifiers => _identifiers; @@ -45,6 +47,7 @@ public class ObjectManager : IReadOnlyDictionary LastUpdate = lastUpdate; World = (ushort)(_clientState.LocalPlayer?.CurrentWorld.Id ?? 0u); _identifiers.Clear(); + _allWorldIdentifiers.Clear(); for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i) { @@ -106,6 +109,23 @@ public class ObjectManager : IReadOnlyDictionary { data.Objects.Add(character); } + + if (identifier.Type is not (IdentifierType.Player or IdentifierType.Owned)) + return; + + var allWorld = _actors.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, + identifier.Kind, + identifier.DataId); + + if (!_allWorldIdentifiers.TryGetValue(allWorld, out var allWorldData)) + { + allWorldData = new ActorData(character, allWorld.ToString()); + _allWorldIdentifiers[allWorld] = allWorldData; + } + else + { + allWorldData.Objects.Add(character); + } } public Actor GPosePlayer @@ -114,6 +134,9 @@ public class ObjectManager : IReadOnlyDictionary public Actor Player => _objects.GetObjectAddress(0); + public Actor Target + => _targets.Target?.Address ?? nint.Zero; + public (ActorIdentifier Identifier, ActorData Data) PlayerData { get @@ -121,7 +144,18 @@ public class ObjectManager : IReadOnlyDictionary Update(); return Player.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data) ? (ident, data) - : (ActorIdentifier.Invalid, ActorData.Invalid); + : (ident, ActorData.Invalid); + } + } + + public (ActorIdentifier Identifier, ActorData Data) TargetData + { + get + { + Update(); + return Target.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data) + ? (ident, data) + : (ident, ActorData.Invalid); } } @@ -134,15 +168,16 @@ public class ObjectManager : IReadOnlyDictionary public int Count => Identifiers.Count; - /// Also (inefficiently) handles All Worlds players. + /// Also handles All Worlds players. public bool ContainsKey(ActorIdentifier key) - => Identifiers.ContainsKey(key) - || key.HomeWorld == ushort.MaxValue - && Identifiers.Keys.FirstOrDefault(i => i.Type is IdentifierType.Player && i.PlayerName == key.PlayerName).IsValid; + => Identifiers.ContainsKey(key) || _allWorldIdentifiers.ContainsKey(key); public bool TryGetValue(ActorIdentifier key, out ActorData value) => Identifiers.TryGetValue(key, out value); + public bool TryGetValueAllWorld(ActorIdentifier key, out ActorData value) + => _allWorldIdentifiers.TryGetValue(key, out value); + public ActorData this[ActorIdentifier key] => Identifiers[key]; diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs index 124363c..2e74630 100644 --- a/Glamourer/Interop/Penumbra/PenumbraService.cs +++ b/Glamourer/Interop/Penumbra/PenumbraService.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; using Dalamud.Logging; using Dalamud.Plugin; using Glamourer.Interop.Structs; @@ -8,21 +12,52 @@ using Penumbra.Api.Helpers; namespace Glamourer.Interop.Penumbra; +using CurrentSettings = ValueTuple>, bool)?>; + +public readonly record struct Mod(string Name, string DirectoryName) : IComparable +{ + public int CompareTo(Mod other) + { + var nameComparison = string.Compare(Name, other.Name, StringComparison.Ordinal); + if (nameComparison != 0) + return nameComparison; + + return string.Compare(DirectoryName, other.DirectoryName, StringComparison.Ordinal); + } +} + +public readonly record struct ModSettings(IDictionary> Settings, int Priority, bool Enabled) +{ + public ModSettings() + : this(new Dictionary>(), 0, false) + { } + + public static ModSettings Empty + => new(); +} + public unsafe class PenumbraService : IDisposable { public const int RequiredPenumbraBreakingVersion = 4; public const int RequiredPenumbraFeatureVersion = 15; - private readonly DalamudPluginInterface _pluginInterface; - private readonly EventSubscriber _tooltipSubscriber; - private readonly EventSubscriber _clickSubscriber; - private readonly EventSubscriber _creatingCharacterBase; - private readonly EventSubscriber _createdCharacterBase; - private readonly EventSubscriber _modSettingChanged; - private ActionSubscriber _redrawSubscriber; - private FuncSubscriber _drawObjectInfo; - private FuncSubscriber _cutsceneParent; - private FuncSubscriber _objectCollection; + private readonly DalamudPluginInterface _pluginInterface; + private readonly EventSubscriber _tooltipSubscriber; + private readonly EventSubscriber _clickSubscriber; + private readonly EventSubscriber _creatingCharacterBase; + private readonly EventSubscriber _createdCharacterBase; + private readonly EventSubscriber _modSettingChanged; + private ActionSubscriber _redrawSubscriber; + private FuncSubscriber _drawObjectInfo; + private FuncSubscriber _cutsceneParent; + private FuncSubscriber _objectCollection; + private FuncSubscriber> _getMods; + private FuncSubscriber _currentCollection; + private FuncSubscriber _getCurrentSettings; + private FuncSubscriber _setMod; + private FuncSubscriber _setModPriority; + private FuncSubscriber _setModSetting; + private FuncSubscriber, PenumbraApiEc> _setModSettings; private readonly EventSubscriber _initializedEvent; private readonly EventSubscriber _disposedEvent; @@ -72,6 +107,90 @@ public unsafe class PenumbraService : IDisposable remove => _modSettingChanged.Event -= value; } + public IReadOnlyList<(Mod Mod, ModSettings Settings)> GetMods() + { + if (!Available) + return Array.Empty<(Mod Mod, ModSettings Settings)>(); + + try + { + var allMods = _getMods.Invoke(); + var collection = _currentCollection.Invoke(ApiCollectionType.Current); + return allMods + .Select(m => (m.Item1, m.Item2, _getCurrentSettings.Invoke(collection, m.Item1, m.Item2, true))) + .Where(t => t.Item3.Item1 is PenumbraApiEc.Success) + .Select(t => (new Mod(t.Item2, t.Item1), + !t.Item3.Item2.HasValue + ? ModSettings.Empty + : new ModSettings(t.Item3.Item2!.Value.Item3, t.Item3.Item2!.Value.Item2, t.Item3.Item2!.Value.Item1))) + .OrderByDescending(p => p.Item2.Enabled) + .ThenBy(p => p.Item1.Name) + .ThenBy(p => p.Item1.DirectoryName) + .ThenByDescending(p => p.Item2.Priority) + .ToList(); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Error fetching mods from Penumbra:\n{ex}"); + return Array.Empty<(Mod Mod, ModSettings Settings)>(); + } + } + + public string CurrentCollection + => Available ? _currentCollection.Invoke(ApiCollectionType.Current) : ""; + + /// + /// Try to set all mod settings as desired. Only sets when the mod should be enabled. + /// If it is disabled, ignore all other settings. + /// + public string SetMod(Mod mod, ModSettings settings) + { + if (!Available) + return "Penumbra is not available."; + + var sb = new StringBuilder(); + try + { + var collection = _currentCollection.Invoke(ApiCollectionType.Current); + var ec = _setMod.Invoke(collection, mod.DirectoryName, mod.Name, settings.Enabled); + if (ec is PenumbraApiEc.ModMissing) + return $"The mod {mod.Name} [{mod.DirectoryName}] could not be found."; + + Debug.Assert(ec is not PenumbraApiEc.CollectionMissing, "Missing collection should not be possible."); + + if (!settings.Enabled) + return string.Empty; + + ec = _setModPriority.Invoke(collection, mod.DirectoryName, mod.Name, settings.Priority); + Debug.Assert(ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged, "Setting Priority should not be able to fail."); + + foreach (var (setting, list) in settings.Settings) + { + ec = list.Count == 1 + ? _setModSetting.Invoke(collection, mod.DirectoryName, mod.Name, setting, list[0]) + : _setModSettings.Invoke(collection, mod.DirectoryName, mod.Name, setting, (IReadOnlyList)list); + switch (ec) + { + case PenumbraApiEc.OptionGroupMissing: + sb.AppendLine($"Could not find the option group {setting} in mod {mod.Name}."); + break; + case PenumbraApiEc.OptionMissing: + sb.AppendLine($"Could not find all desired options in the option group {setting} in mod {mod.Name}."); + break; + } + + Debug.Assert(ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged, + "Missing Mod or Collection should not be possible here."); + } + + return sb.ToString(); + } + catch (Exception ex) + { + return sb.AppendLine(ex.Message).ToString(); + } + } + /// Obtain the name of the collection currently assigned to the player. public string GetCurrentPlayerCollection() { @@ -123,11 +242,19 @@ public unsafe class PenumbraService : IDisposable _creatingCharacterBase.Enable(); _createdCharacterBase.Enable(); _modSettingChanged.Enable(); - _drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface); - _cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface); - _redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface); - _objectCollection = Ipc.GetCollectionForObject.Subscriber(_pluginInterface); - Available = true; + _drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface); + _cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface); + _redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface); + _objectCollection = Ipc.GetCollectionForObject.Subscriber(_pluginInterface); + _getMods = Ipc.GetMods.Subscriber(_pluginInterface); + _currentCollection = Ipc.GetCollectionForType.Subscriber(_pluginInterface); + _getCurrentSettings = Ipc.GetCurrentModSettings.Subscriber(_pluginInterface); + _setMod = Ipc.TrySetMod.Subscriber(_pluginInterface); + _setModPriority = Ipc.TrySetModPriority.Subscriber(_pluginInterface); + _setModSetting = Ipc.TrySetModSetting.Subscriber(_pluginInterface); + _setModSettings = Ipc.TrySetModSettings.Subscriber(_pluginInterface); + + Available = true; Glamourer.Log.Debug("Glamourer attached to Penumbra."); } catch (Exception e) diff --git a/Glamourer/Services/BackupService.cs b/Glamourer/Services/BackupService.cs index 39e9ce2..25a6e8c 100644 --- a/Glamourer/Services/BackupService.cs +++ b/Glamourer/Services/BackupService.cs @@ -7,12 +7,22 @@ namespace Glamourer.Services; public class BackupService { + private readonly Logger _logger; + private readonly DirectoryInfo _configDirectory; + private readonly IReadOnlyList _fileNames; + public BackupService(Logger logger, FilenameService fileNames) { - var files = GlamourerFiles(fileNames); - Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); + _logger = logger; + _fileNames = GlamourerFiles(fileNames); + _configDirectory = new DirectoryInfo(fileNames.ConfigDirectory); + Backup.CreateAutomaticBackup(logger, _configDirectory, _fileNames); } + /// Create a permanent backup with a given name for migrations. + public void CreateMigrationBackup(string name) + => Backup.CreatePermanentBackup(_logger, _configDirectory, _fileNames, name); + /// Collect all relevant files for glamourer configuration. private static IReadOnlyList GlamourerFiles(FilenameService fileNames) { diff --git a/Glamourer/Services/ConfigMigrationService.cs b/Glamourer/Services/ConfigMigrationService.cs index 762dde9..bb3dbcc 100644 --- a/Glamourer/Services/ConfigMigrationService.cs +++ b/Glamourer/Services/ConfigMigrationService.cs @@ -10,14 +10,16 @@ public class ConfigMigrationService { private readonly SaveService _saveService; private readonly FixedDesignMigrator _fixedDesignMigrator; + private readonly BackupService _backupService; private Configuration _config = null!; private JObject _data = null!; - public ConfigMigrationService(SaveService saveService, FixedDesignMigrator fixedDesignMigrator) + public ConfigMigrationService(SaveService saveService, FixedDesignMigrator fixedDesignMigrator, BackupService backupService) { _saveService = saveService; _fixedDesignMigrator = fixedDesignMigrator; + _backupService = backupService; } public void Migrate(Configuration config) @@ -39,6 +41,7 @@ public class ConfigMigrationService if (_config.Version > 1) return; + _backupService.CreateMigrationBackup("pre_v1_to_v2_migration"); _fixedDesignMigrator.Migrate(_data["FixedDesigns"]); _config.Version = 2; var customizationColor = _data["CustomizationColor"]?.ToObject() ?? ColorId.CustomizationDesign.Data().DefaultColor; diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 4db7874..3c53aa1 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -18,6 +18,7 @@ using Glamourer.Unlocks; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; +using Penumbra.GameData.Data; namespace Glamourer.Services; @@ -73,7 +74,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddInterop(this IServiceCollection services) => services.AddSingleton() @@ -93,7 +95,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddState(this IServiceCollection services) => services.AddSingleton() @@ -114,6 +117,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index 5500d6a..bd3b93a 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -12,7 +12,7 @@ namespace Glamourer.State; public class ActorState { - public enum MetaFlag + public enum MetaIndex { Wetness = EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices, HatState, @@ -45,6 +45,6 @@ public class ActorState public ref StateChanged.Source this[CustomizeIndex type] => ref _sources[EquipFlagExtensions.NumEquipFlags + (int)type]; - public ref StateChanged.Source this[MetaFlag flag] - => ref _sources[(int)flag]; + public ref StateChanged.Source this[MetaIndex index] + => ref _sources[(int)index]; } diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 0a292cf..12fb106 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -102,16 +102,19 @@ public class StateListener : IDisposable var actor = (Actor)actorPtr; var identifier = actor.GetIdentifier(_actors.AwaitedService); - var modelId = *(uint*)modelPtr; + ref var modelId = ref *(uint*)modelPtr; ref var customize = ref *(Customize*)customizePtr; if (_manager.TryGetValue(identifier, out var state)) { _autoDesignApplier.Reduce(actor, identifier, state); switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr)) { + // TODO handle right case UpdateState.Change: break; case UpdateState.Transformed: break; case UpdateState.NoChange: + + modelId = state.ModelData.ModelId; switch (UpdateBaseData(actor, state, customize)) { case UpdateState.Transformed: break; @@ -128,7 +131,7 @@ public class StateListener : IDisposable } } - _funModule.ApplyFun(actor, new Span((void*) equipDataPtr, 10), ref customize); + _funModule.ApplyFun(actor, new Span((void*)equipDataPtr, 10), ref customize); if (modelId == 0) ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender); } @@ -171,7 +174,7 @@ public class StateListener : IDisposable // Do nothing. But this usually can not happen because the hooked function also writes to game objects later. case UpdateState.Transformed: break; case UpdateState.Change: - if (state[slot, false] is not StateChanged.Source.Fixed) + if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) { state.ModelData.SetItem(slot, state.BaseData.Item(slot)); state[slot, false] = StateChanged.Source.Game; @@ -181,7 +184,7 @@ public class StateListener : IDisposable apply = true; } - if (state[slot, false] is not StateChanged.Source.Fixed) + if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) { state.ModelData.SetStain(slot, state.BaseData.Stain(slot)); state[slot, true] = StateChanged.Source.Game; @@ -246,7 +249,7 @@ public class StateListener : IDisposable // Update model state if not on fixed design. case UpdateState.Change: var apply = false; - if (state[slot, false] is not StateChanged.Source.Fixed) + if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) { state.ModelData.SetItem(slot, state.BaseData.Item(slot)); state[slot, false] = StateChanged.Source.Game; @@ -256,7 +259,7 @@ public class StateListener : IDisposable apply = true; } - if (state[slot, true] is not StateChanged.Source.Fixed) + if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) { state.ModelData.SetStain(slot, state.BaseData.Stain(slot)); state[slot, true] = StateChanged.Source.Game; @@ -364,7 +367,7 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state[ActorState.MetaFlag.VisorState] is StateChanged.Source.Fixed) + if (state[ActorState.MetaIndex.VisorState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc) value.Value = state.ModelData.IsVisorToggled(); else _manager.ChangeVisorState(state, value, StateChanged.Source.Game); @@ -394,7 +397,7 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state[ActorState.MetaFlag.HatState] is StateChanged.Source.Fixed) + if (state[ActorState.MetaIndex.HatState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc) value.Value = state.ModelData.IsHatVisible(); else _manager.ChangeHatState(state, value, StateChanged.Source.Game); @@ -424,7 +427,7 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state[ActorState.MetaFlag.WeaponState] is StateChanged.Source.Fixed) + if (state[ActorState.MetaIndex.WeaponState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc) value.Value = state.ModelData.IsWeaponVisible(); else _manager.ChangeWeaponState(state, value, StateChanged.Source.Game); diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 355acac..46c6bd7 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -81,7 +81,7 @@ public class StateManager : IReadOnlyDictionary { ModelData = FromActor(actor, true), BaseData = FromActor(actor, false), - LastJob = (byte) (actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0), + LastJob = (byte)(actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0), }; // state.Identifier is owned. _states.Add(state.Identifier, state); @@ -192,6 +192,21 @@ public class StateManager : IReadOnlyDictionary #region Change Values + /// Turn a non-human actor human. + public void TurnHuman(ActorState state, StateChanged.Source source) + { + if (state.ModelData.ModelId == 0) + return; + + state.ModelData.ModelId = 0; + state[ActorState.MetaIndex.ModelId] = source; + ChangeCustomize(state, Customize.Default, CustomizeFlagExtensions.All, source); + foreach (var slot in EquipSlotExtensions.EqdpSlots) + ChangeEquip(state, slot, ItemManager.NothingItem(slot), 0, source); + ChangeEquip(state, EquipSlot.MainHand, _items.DefaultSword, 0, source); + ChangeEquip(state, EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield), 0, source); + } + /// Change a customization value. public void ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source) { @@ -256,9 +271,14 @@ public class StateManager : IReadOnlyDictionary var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; if (source is StateChanged.Source.Manual) if (type == StateChanged.Type.Equip) - _editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot)); + { + if (slot is not EquipSlot.Head || state.ModelData.IsHatVisible()) + _editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot)); + } else + { _editor.ChangeWeapon(objects, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot)); + } // Meta. Glamourer.Log.Verbose( @@ -283,9 +303,14 @@ public class StateManager : IReadOnlyDictionary var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; if (source is StateChanged.Source.Manual) if (type == StateChanged.Type.Equip) - _editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot)); + { + if (slot is not EquipSlot.Head || state.ModelData.IsHatVisible()) + _editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot)); + } else + { _editor.ChangeWeapon(objects, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot)); + } // Meta. Glamourer.Log.Verbose( @@ -322,7 +347,7 @@ public class StateManager : IReadOnlyDictionary // Update state data. var old = state.ModelData.IsHatVisible(); state.ModelData.SetHatVisible(value); - state[ActorState.MetaFlag.HatState] = source; + state[ActorState.MetaIndex.HatState] = source; // Update draw objects / game objects. _objects.Update(); @@ -333,7 +358,7 @@ public class StateManager : IReadOnlyDictionary // Meta. Glamourer.Log.Verbose( $"Set Head Gear Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.HatState)); + _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaIndex.HatState)); } /// Change weapon visibility. @@ -342,7 +367,7 @@ public class StateManager : IReadOnlyDictionary // Update state data. var old = state.ModelData.IsWeaponVisible(); state.ModelData.SetWeaponVisible(value); - state[ActorState.MetaFlag.WeaponState] = source; + state[ActorState.MetaIndex.WeaponState] = source; // Update draw objects / game objects. _objects.Update(); @@ -353,7 +378,7 @@ public class StateManager : IReadOnlyDictionary // Meta. Glamourer.Log.Verbose( $"Set Weapon Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.WeaponState)); + _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaIndex.WeaponState)); } /// Change visor state. @@ -362,7 +387,7 @@ public class StateManager : IReadOnlyDictionary // Update state data. var old = state.ModelData.IsVisorToggled(); state.ModelData.SetVisor(value); - state[ActorState.MetaFlag.VisorState] = source; + state[ActorState.MetaIndex.VisorState] = source; // Update draw objects. _objects.Update(); @@ -373,7 +398,7 @@ public class StateManager : IReadOnlyDictionary // Meta. Glamourer.Log.Verbose( $"Set Visor State in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.VisorState)); + _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaIndex.VisorState)); } /// Set GPose Wetness. @@ -382,7 +407,7 @@ public class StateManager : IReadOnlyDictionary // Update state data. var old = state.ModelData.IsWet(); state.ModelData.SetIsWet(value); - state[ActorState.MetaFlag.Wetness] = source; + state[ActorState.MetaIndex.Wetness] = source; // Update draw objects / game objects. _objects.Update(); @@ -392,12 +417,12 @@ public class StateManager : IReadOnlyDictionary // Meta. Glamourer.Log.Verbose( $"Set Wetness in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, state[ActorState.MetaFlag.Wetness], state, objects, (old, value, ActorState.MetaFlag.Wetness)); + _event.Invoke(StateChanged.Type.Other, state[ActorState.MetaIndex.Wetness], state, objects, (old, value, ActorState.MetaIndex.Wetness)); } #endregion - public void ApplyDesign(Design design, ActorState state) + public void ApplyDesign(DesignBase design, ActorState state) { void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain) { @@ -416,6 +441,12 @@ public class StateManager : IReadOnlyDictionary } } + if (state.ModelData.ModelId != 0 && design.DesignData.ModelId == 0) + TurnHuman(state, StateChanged.Source.Manual); + + if (design.DoApplyHatVisible()) + ChangeHatState(state, design.DesignData.IsHatVisible(), StateChanged.Source.Manual); + foreach (var slot in EquipSlotExtensions.EqdpSlots) HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot)); @@ -428,8 +459,6 @@ public class StateManager : IReadOnlyDictionary && design.DesignData.Item(EquipSlot.OffHand).Type == state.BaseData.Item(EquipSlot.OffHand).Type, design.DoApplyStain(EquipSlot.OffHand)); - if (design.DoApplyHatVisible()) - ChangeHatState(state, design.DesignData.IsHatVisible(), StateChanged.Source.Manual); if (design.DoApplyWeaponVisible()) ChangeWeaponState(state, design.DesignData.IsWeaponVisible(), StateChanged.Source.Manual); if (design.DoApplyVisorToggle()) diff --git a/Glamourer/Unlocks/ItemUnlockManager.cs b/Glamourer/Unlocks/ItemUnlockManager.cs index 0c41926..fed2cdf 100644 --- a/Glamourer/Unlocks/ItemUnlockManager.cs +++ b/Glamourer/Unlocks/ItemUnlockManager.cs @@ -111,12 +111,12 @@ public class ItemUnlockManager : ISavable, IDisposable scan |= newArmoireState; } - //var newAchievementState = uiState->Achievement.IsAchievementLoaded(); - //if (newAchievementState != _lastAchievementState) - //{ - // _lastAchievementState = newAchievementState; - // scan |= newAchievementState; - //} + var newAchievementState = uiState->Achievement.IsLoaded(); + if (newAchievementState != _lastAchievementState) + { + _lastAchievementState = newAchievementState; + scan |= newAchievementState; + } if (scan) Scan(); diff --git a/Glamourer/Utility/CompressExtensions.cs b/Glamourer/Utility/CompressExtensions.cs new file mode 100644 index 0000000..b3ea9c2 --- /dev/null +++ b/Glamourer/Utility/CompressExtensions.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; +using Penumbra.String.Functions; + +namespace Glamourer.Utility; + +public static class CompressExtensions +{ + /// Compress a byte array with a prepended version. + public static unsafe byte[] Compress(this byte[] data, byte version) + { + using var compressedStream = new MemoryStream(); + using var zipStream = new GZipStream(compressedStream, CompressionMode.Compress); + zipStream.Write(data, 0, data.Length); + zipStream.Flush(); + + var ret = new byte[compressedStream.Length + 1]; + ret[0] = version; + fixed (byte* ptr1 = compressedStream.GetBuffer(), ptr2 = ret) + { + MemoryUtility.MemCpyUnchecked(ptr2 + 1, ptr1, (int)compressedStream.Length); + } + + return ret; + } + + /// Compress a string with a prepended version. + public static byte[] Compress(this string data, byte version) + { + var bytes = Encoding.UTF8.GetBytes(data); + return bytes.Compress(version); + } + + /// Decompress a byte array into a returned version byte and an array of the remaining bytes. + public static byte Decompress(this byte[] compressed, out byte[] decompressed) + { + var ret = compressed[0]; + using var compressedStream = new MemoryStream(compressed, 1, compressed.Length - 1); + using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + using var resultStream = new MemoryStream(); + zipStream.CopyTo(resultStream); + decompressed = resultStream.ToArray(); + return ret; + } + + /// Decompress a byte array into a returned version byte and a string of the remaining bytes as UTF8. + public static byte DecompressToString(this byte[] compressed, out string decompressed) + { + var ret = compressed.Decompress(out var bytes); + decompressed = Encoding.UTF8.GetString(bytes); + return ret; + } +}