diff --git a/.gitmodules b/.gitmodules index f74e14d..7203d22 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,16 +1,16 @@ [submodule "OtterGui"] path = OtterGui - url = git@github.com:Ottermandias/OtterGui.git + url = https://github.com/Ottermandias/OtterGui.git branch = main [submodule "Penumbra.GameData"] path = Penumbra.GameData - url = git@github.com:Ottermandias/Penumbra.GameData.git + url = https://github.com/Ottermandias/Penumbra.GameData.git branch = main [submodule "Penumbra.String"] path = Penumbra.String - url = git@github.com:Ottermandias/Penumbra.String.git + url = https://github.com/Ottermandias/Penumbra.String.git branch = main [submodule "Penumbra.Api"] path = Penumbra.Api - url = git@github.com:Ottermandias/Penumbra.Api.git + url = https://github.com/Ottermandias/Penumbra.Api.git branch = main diff --git a/Glamourer.GameData/Structs/CrestFlag.cs b/Glamourer.GameData/Structs/CrestFlag.cs new file mode 100644 index 0000000..61ccc7e --- /dev/null +++ b/Glamourer.GameData/Structs/CrestFlag.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Penumbra.GameData.Enums; + +namespace Glamourer.Structs; + +[Flags] +public enum CrestFlag : ushort +{ + OffHand = 0x0001, + Head = 0x0002, + Body = 0x0004, + Hands = 0x0008, + Legs = 0x0010, + Feet = 0x0020, + Ears = 0x0040, + Neck = 0x0080, + Wrists = 0x0100, + RFinger = 0x0200, + LFinger = 0x0400, + MainHand = 0x0800, +} + +public enum CrestType : byte +{ + None, + Human, + Mainhand, + Offhand, +}; + +public static class CrestExtensions +{ + public const CrestFlag All = (CrestFlag)(((ulong)EquipFlag.Mainhand << 1) - 1); + public const CrestFlag AllRelevant = CrestFlag.Head | CrestFlag.Body | CrestFlag.OffHand; + + public static readonly IReadOnlyList AllRelevantSet = Enum.GetValues().Where(f => AllRelevant.HasFlag(f)).ToArray(); + + public static int ToInternalIndex(this CrestFlag flag) + => flag switch + { + CrestFlag.Head => 0, + CrestFlag.Body => 1, + CrestFlag.OffHand => 2, + _ => -1, + }; + + public static (CrestType Type, byte Index) ToIndex(this CrestFlag flag) + => flag switch + { + CrestFlag.Head => (CrestType.Human, 0), + CrestFlag.Body => (CrestType.Human, 1), + CrestFlag.Hands => (CrestType.Human, 2), + CrestFlag.Legs => (CrestType.Human, 3), + CrestFlag.Feet => (CrestType.Human, 4), + CrestFlag.Ears => (CrestType.None, 0), + CrestFlag.Neck => (CrestType.None, 0), + CrestFlag.Wrists => (CrestType.None, 0), + CrestFlag.RFinger => (CrestType.None, 0), + CrestFlag.LFinger => (CrestType.None, 0), + CrestFlag.MainHand => (CrestType.None, 0), + CrestFlag.OffHand => (CrestType.Offhand, 0), + _ => (CrestType.None, 0), + }; + + public static CrestFlag ToCrestFlag(this EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => CrestFlag.MainHand, + EquipSlot.OffHand => CrestFlag.OffHand, + EquipSlot.Head => CrestFlag.Head, + EquipSlot.Body => CrestFlag.Body, + EquipSlot.Hands => CrestFlag.Hands, + EquipSlot.Legs => CrestFlag.Legs, + EquipSlot.Feet => CrestFlag.Feet, + EquipSlot.Ears => CrestFlag.Ears, + EquipSlot.Neck => CrestFlag.Neck, + EquipSlot.Wrists => CrestFlag.Wrists, + EquipSlot.RFinger => CrestFlag.RFinger, + EquipSlot.LFinger => CrestFlag.LFinger, + _ => 0, + }; + + public static string ToLabel(this CrestFlag flag) + => flag switch + { + CrestFlag.Head => "Head", + CrestFlag.Body => "Chest", + CrestFlag.Hands => "Gauntlets", + CrestFlag.Legs => "Pants", + CrestFlag.Feet => "Boot", + CrestFlag.Ears => "Earrings", + CrestFlag.Neck => "Necklace", + CrestFlag.Wrists => "Bracelet", + CrestFlag.RFinger => "Right Ring", + CrestFlag.LFinger => "Left Ring", + CrestFlag.MainHand => "Weapon", + CrestFlag.OffHand => "Shield", + _ => string.Empty, + }; +} diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs index 7beda18..0a26759 100644 --- a/Glamourer/Automation/AutoDesign.cs +++ b/Glamourer/Automation/AutoDesign.cs @@ -15,18 +15,19 @@ public class AutoDesign [Flags] public enum Type : byte { - Armor = 0x01, - Customizations = 0x02, - Weapons = 0x04, - Stains = 0x08, - Accessories = 0x10, + Armor = 0x01, + Customizations = 0x02, + Weapons = 0x04, + GearCustomization = 0x08, + Accessories = 0x10, - All = Armor | Accessories | Customizations | Weapons | Stains, + All = Armor | Accessories | Customizations | Weapons | GearCustomization, } public Design? Design; public JobGroup Jobs; public Type ApplicationType; + public short GearsetIndex = -1; public string Name(bool incognito) => Revert ? RevertName : incognito ? Design!.Incognito : Design!.Name.Text; @@ -43,10 +44,22 @@ public class AutoDesign Design = Design, ApplicationType = ApplicationType, Jobs = Jobs, + GearsetIndex = GearsetIndex, }; public unsafe bool IsActive(Actor actor) - => actor.IsCharacter && Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob); + { + if (!actor.IsCharacter) + return false; + + var ret = true; + if (GearsetIndex < 0) + ret &= Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob); + else + ret &= AutoDesignApplier.CheckGearset(GearsetIndex); + + return ret; + } public JObject Serialize() => new() @@ -58,25 +71,29 @@ public class AutoDesign private JObject CreateConditionObject() { - var ret = new JObject(); - if (Jobs.Id != 0) - ret["JobGroup"] = Jobs.Id; + var ret = new JObject + { + ["Gearset"] = GearsetIndex, + ["JobGroup"] = Jobs.Id, + }; + return ret; } - public (EquipFlag Equip, CustomizeFlag Customize, bool ApplyHat, bool ApplyVisor, bool ApplyWeapon, bool ApplyWet) ApplyWhat() + public (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, bool ApplyHat, bool ApplyVisor, bool ApplyWeapon, bool ApplyWet) ApplyWhat() { var equipFlags = (ApplicationType.HasFlag(Type.Weapons) ? WeaponFlags : 0) | (ApplicationType.HasFlag(Type.Armor) ? ArmorFlags : 0) | (ApplicationType.HasFlag(Type.Accessories) ? AccessoryFlags : 0) - | (ApplicationType.HasFlag(Type.Stains) ? StainFlags : 0); + | (ApplicationType.HasFlag(Type.GearCustomization) ? StainFlags : 0); var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0; + var crestFlag = ApplicationType.HasFlag(Type.GearCustomization) ? CrestExtensions.AllRelevant : 0; if (Revert) - return (equipFlags, customizeFlags, ApplicationType.HasFlag(Type.Armor), ApplicationType.HasFlag(Type.Armor), + return (equipFlags, customizeFlags, crestFlag, ApplicationType.HasFlag(Type.Armor), ApplicationType.HasFlag(Type.Armor), ApplicationType.HasFlag(Type.Weapons), ApplicationType.HasFlag(Type.Customizations)); - return (equipFlags & Design!.ApplyEquip, customizeFlags & Design.ApplyCustomize, + return (equipFlags & Design!.ApplyEquip, customizeFlags & Design.ApplyCustomize, crestFlag & Design.ApplyCrest, ApplicationType.HasFlag(Type.Armor) && Design.DoApplyHatVisible(), ApplicationType.HasFlag(Type.Armor) && Design.DoApplyVisorToggle(), ApplicationType.HasFlag(Type.Weapons) && Design.DoApplyWeaponVisible(), diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 92cd2b0..ddc8636 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; @@ -26,6 +27,7 @@ public class AutoDesignApplier : IDisposable private readonly AutoDesignManager _manager; private readonly StateManager _state; private readonly JobService _jobs; + private readonly EquippedGearset _equippedGearset; private readonly ActorService _actors; private readonly CustomizationService _customizations; private readonly CustomizeUnlockManager _customizeUnlocks; @@ -49,7 +51,8 @@ public class AutoDesignApplier : IDisposable public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, - AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState) + AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState, + EquippedGearset equippedGearset) { _config = config; _manager = manager; @@ -64,15 +67,18 @@ public class AutoDesignApplier : IDisposable _weapons = weapons; _humans = humans; _clientState = clientState; + _equippedGearset = equippedGearset; _jobs.JobChanged += OnJobChange; _event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier); _weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier); + _equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.Priority.AutoDesignApplier); } public void Dispose() { _weapons.Unsubscribe(OnWeaponLoading); _event.Unsubscribe(OnAutomationChange); + _equippedGearset.Unsubscribe(OnEquippedGearset); _jobs.JobChanged -= OnJobChange; } @@ -262,6 +268,7 @@ public class AutoDesignApplier : IDisposable { EquipFlag totalEquipFlags = 0; CustomizeFlag totalCustomizeFlags = 0; + CrestFlag totalCrestFlags = 0; byte totalMetaFlags = 0; if (set.BaseState == AutoDesignSet.Base.Game) _state.ResetStateFixed(state); @@ -285,10 +292,11 @@ public class AutoDesignApplier : IDisposable if (!data.IsHuman) continue; - var (equipFlags, customizeFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat(); + var (equipFlags, customizeFlags, crestFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat(); ReduceMeta(state, data, applyHat, applyVisor, applyWeapon, applyWet, ref totalMetaFlags, respectManual, source); ReduceCustomize(state, data, customizeFlags, ref totalCustomizeFlags, respectManual, source); ReduceEquip(state, data, equipFlags, ref totalEquipFlags, respectManual, source, fromJobChange); + ReduceCrests(state, data, crestFlags, ref totalCrestFlags, respectManual, source); } if (totalCustomizeFlags != 0) @@ -318,6 +326,24 @@ public class AutoDesignApplier : IDisposable } } + private void ReduceCrests(ActorState state, in DesignData design, CrestFlag crestFlags, ref CrestFlag totalCrestFlags, bool respectManual, + StateChanged.Source source) + { + crestFlags &= ~totalCrestFlags; + if (crestFlags == 0) + return; + + foreach (var slot in CrestExtensions.AllRelevantSet) + { + if (!crestFlags.HasFlag(slot)) + continue; + + if (!respectManual || state[slot] is not StateChanged.Source.Manual) + _state.ChangeCrest(state, slot, design.Crest(slot), source); + totalCrestFlags |= slot; + } + } + private void ReduceEquip(ActorState state, in DesignData design, EquipFlag equipFlags, ref EquipFlag totalEquipFlags, bool respectManual, StateChanged.Source source, bool fromJobChange) { @@ -496,4 +522,38 @@ public class AutoDesignApplier : IDisposable totalMetaFlags |= 0x08; } } + + internal static int NewGearsetId = -1; + + private void OnEquippedGearset(string name, int id, int prior, byte _, byte job) + { + if (!_config.EnableAutoDesigns) + return; + + var (player, data) = _objects.PlayerData; + if (!player.IsValid) + return; + + if (!GetPlayerSet(player, out var set) || !_state.TryGetValue(player, out var state)) + return; + + var respectManual = prior == id; + NewGearsetId = id; + Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob); + NewGearsetId = -1; + foreach (var actor in data.Objects) + _state.ReapplyState(actor); + } + + public static unsafe bool CheckGearset(short check) + { + if (NewGearsetId != -1) + return check == NewGearsetId; + + var module = RaptureGearsetModule.Instance(); + if (module == null) + return false; + + return check == module->CurrentGearsetIndex; + } } diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index 8140084..d3fba5c 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -306,6 +306,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos return; var design = set.Designs[which]; + if (design.Jobs.Id == jobs.Id) return; @@ -316,6 +317,22 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos _event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, jobs)); } + public void ChangeGearsetCondition(AutoDesignSet set, int which, short index) + { + if (which >= set.Designs.Count || which < 0) + return; + + var design = set.Designs[which]; + if (design.GearsetIndex == index) + return; + + var old = design.GearsetIndex; + design.GearsetIndex = index; + Save(); + Glamourer.Log.Debug($"Changed gearset condition from {old} to {index} for associated design {which + 1} in design set."); + _event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, index)); + } + public void ChangeApplicationType(AutoDesignSet set, int which, AutoDesign.Type type) { if (which >= set.Designs.Count || which < 0) @@ -338,10 +355,8 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos public void Save(StreamWriter writer) { - using var j = new JsonTextWriter(writer) - { - Formatting = Formatting.Indented, - }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; Serialize().WriteTo(j); } @@ -456,13 +471,16 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos { if (designIdentifier.Length == 0) { - Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.", NotificationType.Warning); + Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.", + NotificationType.Warning); return null; } if (!Guid.TryParse(designIdentifier, out var guid)) { - Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.", NotificationType.Warning); + Glamourer.Messager.NotificationMessage( + $"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.", + NotificationType.Warning); return null; } @@ -471,7 +489,8 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos if (design == null) { Glamourer.Messager.NotificationMessage( - $"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.", NotificationType.Warning); + $"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.", + NotificationType.Warning); return null; } } @@ -483,24 +502,31 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos Design = design, ApplicationType = applicationType & AutoDesign.Type.All, }; + return ParseConditions(setName, jObj, ret) ? ret : null; + } + private bool ParseConditions(string setName, JObject jObj, AutoDesign ret) + { var conditions = jObj["Conditions"]; if (conditions == null) - return ret; + return true; var jobs = conditions["JobGroup"]?.ToObject() ?? -1; if (jobs >= 0) { if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup)) { - Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.", NotificationType.Warning); - return null; + Glamourer.Messager.NotificationMessage( + $"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.", + NotificationType.Warning); + return false; } ret.Jobs = jobGroup; } - return ret; + ret.GearsetIndex = conditions["Gearset"]?.ToObject() ?? -1; + return true; } private void Save() diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 5c106e3..d10fe29 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Interface.Internal.Notifications; -using Glamourer.Customization; -using Glamourer.Gui; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Newtonsoft.Json; @@ -28,8 +26,8 @@ public sealed class Design : DesignBase, ISavable internal Design(Design other) : base(other) { - Tags = Tags.ToArray(); - Description = Description; + Tags = other.Tags.ToArray(); + Description = other.Description; AssociatedMods = new SortedList(other.AssociatedMods); } @@ -69,8 +67,7 @@ public sealed class Design : DesignBase, ISavable ["Equipment"] = SerializeEquipment(), ["Customize"] = SerializeCustomize(), ["Mods"] = SerializeMods(), - } - ; + }; return ret; } diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index 183ca99..d859e8e 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Internal.Notifications; using Glamourer.Customization; using Glamourer.Services; using Glamourer.Structs; @@ -9,6 +7,8 @@ using OtterGui.Classes; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using System; +using System.Linq; namespace Glamourer.Designs; @@ -39,7 +39,6 @@ public class DesignBase ApplyEquip = equipFlags & EquipFlagExtensions.All; _designFlags = 0; CustomizationSet = SetCustomizationSet(customize); - } internal DesignBase(DesignBase clone) @@ -83,6 +82,7 @@ public class DesignBase => _applyCustomize; internal EquipFlag ApplyEquip = EquipFlagExtensions.All; + internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; public bool SetCustomize(CustomizationService customizationService, Customize customize) @@ -169,6 +169,9 @@ public class DesignBase public bool DoApplyCustomize(CustomizeIndex idx) => ApplyCustomize.HasFlag(idx.ToFlag()); + public bool DoApplyCrest(CrestFlag slot) + => ApplyCrest.HasFlag(slot); + internal bool SetApplyEquip(EquipSlot slot, bool value) { var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag(); @@ -199,28 +202,42 @@ public class DesignBase return true; } - internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags) - => new(this, equipFlags, customizeFlags); + internal bool SetApplyCrest(CrestFlag slot, bool value) + { + var newValue = value ? ApplyCrest | slot : ApplyCrest & ~slot; + if (newValue == ApplyCrest) + return false; + + ApplyCrest = newValue; + return true; + } + + internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags) + => new(this, equipFlags, customizeFlags, crestFlags); internal readonly struct FlagRestrictionResetter : IDisposable { private readonly DesignBase _design; private readonly EquipFlag _oldEquipFlags; private readonly CustomizeFlag _oldCustomizeFlags; + private readonly CrestFlag _oldCrestFlags; - public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags) + public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags) { _design = d; _oldEquipFlags = d.ApplyEquip; _oldCustomizeFlags = d.ApplyCustomizeRaw; + _oldCrestFlags = d.ApplyCrest; d.ApplyEquip &= equipFlags; d.ApplyCustomize &= customizeFlags; + d.ApplyCrest &= crestFlags; } public void Dispose() { _design.ApplyEquip = _oldEquipFlags; _design.ApplyCustomize = _oldCustomizeFlags; + _design.ApplyCrest = _oldCrestFlags; } } @@ -246,13 +263,15 @@ public class DesignBase protected JObject SerializeEquipment() { - static JObject Serialize(CustomItemId id, StainId stain, bool apply, bool applyStain) + static JObject Serialize(CustomItemId id, StainId stain, bool crest, bool apply, bool applyStain, bool applyCrest) => new() { ["ItemId"] = id.Id, ["Stain"] = stain.Id, + ["Crest"] = crest, ["Apply"] = apply, ["ApplyStain"] = applyStain, + ["ApplyCrest"] = applyCrest, }; var ret = new JObject(); @@ -260,9 +279,11 @@ public class DesignBase { 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)); + var item = _designData.Item(slot); + var stain = _designData.Stain(slot); + var crestSlot = slot.ToCrestFlag(); + var crest = _designData.Crest(crestSlot); + ret[slot.ToString()] = Serialize(item.Id, stain, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot)); } ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply"); @@ -345,13 +366,15 @@ public class DesignBase return; } - static (CustomItemId, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item) + static (CustomItemId, StainId, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item) { var id = item?["ItemId"]?.ToObject() ?? ItemManager.NothingId(slot).Id; var stain = (StainId)(item?["Stain"]?.ToObject() ?? 0); + var crest = item?["Crest"]?.ToObject() ?? false; var apply = item?["Apply"]?.ToObject() ?? false; var applyStain = item?["ApplyStain"]?.ToObject() ?? false; - return (id, stain, apply, applyStain); + var applyCrest = item?["ApplyCrest"]?.ToObject() ?? false; + return (id, stain, crest, apply, applyStain, applyCrest); } void PrintWarning(string msg) @@ -362,21 +385,25 @@ public class DesignBase foreach (var slot in EquipSlotExtensions.EqdpSlots) { - var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]); + var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]); PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown)); PrintWarning(items.ValidateStain(stain, out stain, allowUnknown)); + var crestSlot = slot.ToCrestFlag(); design._designData.SetItem(slot, item); design._designData.SetStain(slot, stain); + design._designData.SetCrest(crestSlot, crest); design.SetApplyEquip(slot, apply); design.SetApplyStain(slot, applyStain); + design.SetApplyCrest(crestSlot, applyCrest); } { - var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); + var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); if (id == ItemManager.NothingId(EquipSlot.MainHand)) id = items.DefaultSword.ItemId; - var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); + var (idOff, stainOff, crestOff, applyOff, applyStainOff, applyCrestOff) = + ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); if (id == ItemManager.NothingId(EquipSlot.OffHand)) id = ItemManager.NothingId(FullEquipType.Shield); @@ -387,10 +414,14 @@ public class DesignBase design._designData.SetItem(EquipSlot.OffHand, off); design._designData.SetStain(EquipSlot.MainHand, stain); design._designData.SetStain(EquipSlot.OffHand, stainOff); + design._designData.SetCrest(CrestFlag.MainHand, crest); + design._designData.SetCrest(CrestFlag.OffHand, crestOff); design.SetApplyEquip(EquipSlot.MainHand, apply); design.SetApplyEquip(EquipSlot.OffHand, applyOff); design.SetApplyStain(EquipSlot.MainHand, applyStain); design.SetApplyStain(EquipSlot.OffHand, applyStainOff); + design.SetApplyCrest(CrestFlag.MainHand, applyCrest); + design.SetApplyCrest(CrestFlag.OffHand, applyCrestOff); } var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse); design.SetApplyHatVisible(metaValue.Enabled); diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs index e8b1742..6ab6901 100644 --- a/Glamourer/Designs/DesignConverter.cs +++ b/Glamourer/Designs/DesignConverter.cs @@ -13,22 +13,9 @@ using Penumbra.GameData.Enums; namespace Glamourer.Designs; -public class DesignConverter +public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizationService _customize, HumanModelList _humans) { - public const byte Version = 5; - - private readonly ItemManager _items; - private readonly DesignManager _designs; - private readonly CustomizationService _customize; - private readonly HumanModelList _humans; - - public DesignConverter(ItemManager items, DesignManager designs, CustomizationService customize, HumanModelList humans) - { - _items = items; - _designs = designs; - _customize = customize; - _humans = humans; - } + public const byte Version = 6; public JObject ShareJObject(DesignBase design) => design.JsonSerialize(); @@ -36,32 +23,33 @@ public class DesignConverter public JObject ShareJObject(Design design) => design.JsonSerialize(); - public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags) + public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags) { - var design = Convert(state, equipFlags, customizeFlags); + var design = Convert(state, equipFlags, customizeFlags, crestFlags); return ShareJObject(design); } public string ShareBase64(Design design) - => ShareBackwardCompatible(ShareJObject(design), design); + => ShareBase64(ShareJObject(design)); public string ShareBase64(DesignBase design) - => ShareBackwardCompatible(ShareJObject(design), design); + => ShareBase64(ShareJObject(design)); public string ShareBase64(ActorState state) - => ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All); + => ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All, CrestExtensions.All); - public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags) + public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags) { - var design = Convert(state, equipFlags, customizeFlags); - return ShareBackwardCompatible(ShareJObject(design), design); + var design = Convert(state, equipFlags, customizeFlags, crestFlags); + return ShareBase64(ShareJObject(design)); } - public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags) + public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags) { var design = _designs.CreateTemporary(); design.ApplyEquip = equipFlags & EquipFlagExtensions.All; - design.ApplyCustomize = customizeFlags; + design.ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; + design.ApplyCrest = crestFlags & CrestExtensions.All; design.SetApplyHatVisible(design.DoApplyEquip(EquipSlot.Head)); design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head)); design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand)); @@ -123,6 +111,7 @@ public class DesignConverter : DesignBase.LoadDesignBase(_customize, _items, jObj2); break; } + default: throw new Exception($"Unknown Version {bytes[0]}."); } } @@ -138,6 +127,7 @@ public class DesignConverter if (!equip) { ret.ApplyEquip = 0; + ret.ApplyCrest = 0; ret.SetApplyHatVisible(false); ret.SetApplyWeaponVisible(false); ret.SetApplyVisorToggle(false); @@ -146,23 +136,10 @@ public class DesignConverter return ret; } - private static string ShareBase64(JObject jObj) + private static string ShareBase64(JObject jObject) { - var json = jObj.ToString(Formatting.None); + var json = jObject.ToString(Formatting.None); var compressed = json.Compress(Version); return System.Convert.ToBase64String(compressed); } - - private static string ShareBackwardCompatible(JObject jObject, DesignBase design) - { - var oldBase64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomizeRaw, - design.DoApplyHatVisible(), design.DoApplyVisorToggle(), design.DoApplyWeaponVisible(), design.WriteProtected(), 1f); - var oldBytes = System.Convert.FromBase64String(oldBase64); - var json = jObject.ToString(Formatting.None); - var compressed = json.Compress(Version); - var bytes = new byte[oldBytes.Length + compressed.Length]; - oldBytes.CopyTo(bytes, 0); - compressed.CopyTo(bytes, oldBytes.Length); - return System.Convert.ToBase64String(bytes); - } } diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs index 4a24f59..4b0d53b 100644 --- a/Glamourer/Designs/DesignData.cs +++ b/Glamourer/Designs/DesignData.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using Glamourer.Customization; using Glamourer.Services; +using Glamourer.Structs; using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -29,6 +30,7 @@ public unsafe struct DesignData private fixed byte _equipmentBytes[48]; public Customize Customize = Customize.Default; public uint ModelId; + public CrestFlag CrestVisibility; private WeaponType _secondaryMainhand; private WeaponType _secondaryOffhand; private FullEquipType _typeMainhand; @@ -59,6 +61,10 @@ public unsafe struct DesignData return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3]; } + public readonly bool Crest(CrestFlag slot) + => CrestVisibility.HasFlag(slot); + + public FullEquipType MainhandType => _typeMainhand; @@ -173,6 +179,16 @@ public unsafe struct DesignData _ => false, }; + public bool SetCrest(CrestFlag slot, bool visible) + { + var newValue = visible ? CrestVisibility | slot : CrestVisibility & ~slot; + if (newValue == CrestVisibility) + return false; + + CrestVisibility = newValue; + return true; + } + public readonly bool IsWet() => (_states & 0x01) == 0x01; @@ -228,12 +244,15 @@ public unsafe struct DesignData { SetItem(slot, ItemManager.NothingItem(slot)); SetStain(slot, 0); + SetCrest(slot.ToCrestFlag(), false); } SetItem(EquipSlot.MainHand, items.DefaultSword); SetStain(EquipSlot.MainHand, 0); + SetCrest(CrestFlag.MainHand, false); SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield)); SetStain(EquipSlot.OffHand, 0); + SetCrest(CrestFlag.OffHand, false); } diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index b8cd9a2..392301f 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -8,6 +8,7 @@ using Glamourer.Events; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.State; +using Glamourer.Structs; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; @@ -345,7 +346,7 @@ public class DesignManager /// Change a non-weapon equipment piece. public void ChangeEquip(Design design, EquipSlot slot, EquipItem item) { - if (!_items.IsItemValid(slot, item.ItemId, out item)) + if (!_items.IsItemValid(slot, item.Id, out item)) return; var old = design.DesignData.Item(slot); @@ -446,6 +447,31 @@ public class DesignManager _event.Invoke(DesignChanged.Type.ApplyStain, design, slot); } + /// Change the crest visibility for any equipment piece. + public void ChangeCrest(Design design, CrestFlag slot, bool crest) + { + var oldCrest = design.DesignData.Crest(slot); + if (!design.GetDesignDataRef().SetCrest(slot, crest)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}."); + _event.Invoke(DesignChanged.Type.Crest, design, (oldCrest, crest, slot)); + } + + /// Change whether to apply a specific crest visibility. + public void ChangeApplyCrest(Design design, CrestFlag slot, bool value) + { + if (!design.SetApplyCrest(slot, value)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}."); + _event.Invoke(DesignChanged.Type.ApplyCrest, design, slot); + } + /// Change the bool value of one of the meta flags. public void ChangeMeta(Design design, ActorState.MetaIndex metaIndex, bool value) { @@ -515,6 +541,12 @@ public class DesignManager if (other.DoApplyStain(slot)) ChangeStain(design, slot, other.DesignData.Stain(slot)); } + + foreach (var slot in Enum.GetValues()) + { + if (other.DoApplyCrest(slot)) + ChangeCrest(design, slot, other.DesignData.Crest(slot)); + } } if (other.DoApplyEquip(EquipSlot.MainHand)) diff --git a/Glamourer/EphemeralConfig.cs b/Glamourer/EphemeralConfig.cs index 349a021..4bd57ed 100644 --- a/Glamourer/EphemeralConfig.cs +++ b/Glamourer/EphemeralConfig.cs @@ -66,7 +66,8 @@ public class EphemeralConfig : ISavable public void Save(StreamWriter writer) { - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index 55956f0..c528fde 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -62,6 +62,9 @@ public sealed class DesignChanged : EventWrapper An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. Stain, + /// An existing design had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. + Crest, + /// An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. ApplyCustomize, @@ -71,6 +74,9 @@ 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 whether a specific crest visibility is applied. Data is the slot of the equipment [EquipSlot]. + ApplyCrest, + /// An existing design changed its write protection status. Data is the new value [bool]. WriteProtection, diff --git a/Glamourer/Events/EquippedGearset.cs b/Glamourer/Events/EquippedGearset.cs new file mode 100644 index 0000000..a8fafff --- /dev/null +++ b/Glamourer/Events/EquippedGearset.cs @@ -0,0 +1,30 @@ +using System; +using OtterGui.Classes; + +namespace Glamourer.Events; + +/// +/// Triggered when the player equips a gear set. +/// +/// Parameter is the name of the gear set. +/// Parameter is the id of the gear set. +/// Parameter is the id of the prior gear set. +/// Parameter is the id of the associated glamour. +/// Parameter is the job id of the associated job. +/// +/// +public sealed class EquippedGearset : EventWrapper, EquippedGearset.Priority> +{ + public enum Priority + { + /// + AutoDesignApplier = 0, + } + + public EquippedGearset() + : base(nameof(EquippedGearset)) + { } + + public void Invoke(string name, int id, int lastId, byte glamour, byte jobId) + => Invoke(this, name, id, lastId, glamour, jobId); +} diff --git a/Glamourer/Events/StateChanged.cs b/Glamourer/Events/StateChanged.cs index 2c5c5c8..e02a6d9 100644 --- a/Glamourer/Events/StateChanged.cs +++ b/Glamourer/Events/StateChanged.cs @@ -37,6 +37,9 @@ public sealed class StateChanged : EventWrapper A characters saved state had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. Stain, + /// A characters saved state had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. + Crest, + /// A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] Design, diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs index 3e15cad..e9ce9e9 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -194,14 +194,14 @@ public partial class CustomizationDrawer { switch (UiHelpers.DrawMetaToggle(_currentIndex.ToDefaultName(), tmp, _currentApply, out var newValue, out var newApply, _locked)) { - case DataChange.Item: + case (true, false): _customize.Set(idx, newValue ? CustomizeValue.Max : CustomizeValue.Zero); Changed |= _currentFlag; break; - case DataChange.ApplyItem: + case (false, true): ChangeApply = newApply ? ChangeApply | _currentFlag : ChangeApply & ~_currentFlag; break; - case DataChange.Item | DataChange.ApplyItem: + case (true, true): ChangeApply = newApply ? ChangeApply | _currentFlag : ChangeApply & ~_currentFlag; _customize.Set(idx, newValue ? CustomizeValue.Max : CustomizeValue.Zero); Changed |= _currentFlag; diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.cs b/Glamourer/Gui/Customization/CustomizationDrawer.cs index 0f76b62..fa725f5 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.cs @@ -14,18 +14,16 @@ using CustomizeData = Penumbra.GameData.Structs.CustomizeData; namespace Glamourer.Gui.Customization; -public partial class CustomizationDrawer : IDisposable +public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizationService _service, CodeService _codes, Configuration _config) + : IDisposable { - private readonly CodeService _codes; - private readonly Configuration _config; - - private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f); - private readonly IDalamudTextureWrap? _legacyTattoo; + private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f); + private readonly IDalamudTextureWrap? _legacyTattoo = GetLegacyTattooIcon(pi); private Exception? _terminate; - private Customize _customize; - private CustomizationSet _set = null!; + private Customize _customize = Customize.Default; + private CustomizationSet _set = null!; public Customize Customize => _customize; @@ -46,21 +44,8 @@ public partial class CustomizationDrawer : IDisposable private float _raceSelectorWidth; private bool _withApply; - private readonly CustomizationService _service; - - public CustomizationDrawer(DalamudPluginInterface pi, CustomizationService service, CodeService codes, Configuration config) - { - _service = service; - _codes = codes; - _config = config; - _legacyTattoo = GetLegacyTattooIcon(pi); - _customize = Customize.Default; - } - public void Dispose() - { - _legacyTattoo?.Dispose(); - } + => _legacyTattoo?.Dispose(); public bool Draw(Customize current, bool locked, bool lockedRedraw) { @@ -125,12 +110,6 @@ public partial class CustomizationDrawer : IDisposable Changed |= _currentFlag; } - public bool DrawWetnessState(bool currentValue, out bool newValue, bool locked) - => UiHelpers.DrawCheckbox("Force Wetness", "Force the character to be wet or not.", currentValue, out newValue, locked); - - public DataChange DrawWetnessState(bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) - => UiHelpers.DrawMetaToggle("Force Wetness", currentValue, currentApply, out newValue, out newApply, locked); - private bool DrawInternal() { using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _spacing); @@ -199,13 +178,13 @@ public partial class CustomizationDrawer : IDisposable private void UpdateSizes() { - _spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }; - _iconSize = new Vector2(ImGui.GetTextLineHeight() * 2 + _spacing.Y + 2 * ImGui.GetStyle().FramePadding.Y); - _framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding; - _inputIntSize = 2 * _framedIconSize.X + 1 * _spacing.X; + _spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }; + _iconSize = new Vector2(ImGui.GetTextLineHeight() * 2 + _spacing.Y + 2 * ImGui.GetStyle().FramePadding.Y); + _framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding; + _inputIntSize = 2 * _framedIconSize.X + 1 * _spacing.X; _inputIntSizeNoButtons = _inputIntSize - 2 * _spacing.X - 2 * ImGui.GetFrameHeight(); - _comboSelectorSize = 4 * _framedIconSize.X + 3 * _spacing.X; - _raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X; + _comboSelectorSize = 4 * _framedIconSize.X + 3 * _spacing.X; + _raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X; } private static IDalamudTextureWrap? GetLegacyTattooIcon(DalamudPluginInterface pi) diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs index e6f5eac..68abd0a 100644 --- a/Glamourer/Gui/DesignQuickBar.cs +++ b/Glamourer/Gui/DesignQuickBar.cs @@ -22,8 +22,8 @@ public class DesignQuickBar : Window, IDisposable { private ImGuiWindowFlags GetFlags => _config.Ephemeral.LockDesignQuickBar - ? ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove - : ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoFocusOnAppearing; + ? ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove + : ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing; private readonly Configuration _config; private readonly DesignCombo _designCombo; @@ -37,7 +37,7 @@ public class DesignQuickBar : Window, IDisposable public DesignQuickBar(Configuration config, DesignCombo designCombo, StateManager stateManager, IKeyState keyState, ObjectManager objects, AutoDesignApplier autoDesignApplier) - : base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration) + : base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking) { _config = config; _designCombo = designCombo; @@ -159,8 +159,8 @@ public class DesignQuickBar : Window, IDisposable return; } - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); - using var _ = design!.TemporarilyRestrictApplication(applyGear, applyCustomize); + var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags(); + using var _ = design!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest); _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); } diff --git a/Glamourer/Gui/Equipment/EquipDrawData.cs b/Glamourer/Gui/Equipment/EquipDrawData.cs new file mode 100644 index 0000000..ce2ba04 --- /dev/null +++ b/Glamourer/Gui/Equipment/EquipDrawData.cs @@ -0,0 +1,49 @@ +using System; +using Glamourer.Designs; +using Glamourer.Events; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public ref struct EquipDrawData(EquipSlot slot, in DesignData designData) +{ + public readonly EquipSlot Slot = slot; + public bool Locked; + public bool DisplayApplication; + + public Action ItemSetter = null!; + public Action StainSetter = null!; + public Action ApplySetter = null!; + public Action ApplyStainSetter = null!; + public EquipItem CurrentItem = designData.Item(slot); + public StainId CurrentStain = designData.Stain(slot); + public bool CurrentApply; + public bool CurrentApplyStain; + + public readonly Gender CurrentGender = designData.Customize.Gender; + public readonly Race CurrentRace = designData.Customize.Race; + + public static EquipDrawData FromDesign(DesignManager manager, Design design, EquipSlot slot) + => new(slot, design.DesignData) + { + ItemSetter = i => manager.ChangeEquip(design, slot, i), + StainSetter = i => manager.ChangeStain(design, slot, i), + ApplySetter = b => manager.ChangeApplyEquip(design, slot, b), + ApplyStainSetter = b => manager.ChangeApplyStain(design, slot, b), + CurrentApply = design.DoApplyEquip(slot), + CurrentApplyStain = design.DoApplyStain(slot), + Locked = design.WriteProtected(), + DisplayApplication = true, + }; + + public static EquipDrawData FromState(StateManager manager, ActorState state, EquipSlot slot) + => new(slot, state.ModelData) + { + ItemSetter = i => manager.ChangeItem(state, slot, i, StateChanged.Source.Manual), + StainSetter = i => manager.ChangeStain(state, slot, i, StateChanged.Source.Manual), + Locked = state.IsLocked, + DisplayApplication = false, + }; +} diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs index 3b33f8a..e2b44de 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs @@ -6,10 +6,8 @@ using System.Numerics; using Dalamud.Interface.Components; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; -using Glamourer.Designs; using Glamourer.Events; using Glamourer.Services; -using Glamourer.Structs; using Glamourer.Unlocks; using ImGuiNET; using OtterGui; @@ -76,206 +74,71 @@ public class EquipmentDrawer _requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale; } - private bool VerifyRestrictedGear(EquipSlot slot, EquipItem gear, Gender gender, Race race) + private bool VerifyRestrictedGear(EquipDrawData data) { - if (slot.IsAccessory()) + if (data.Slot.IsAccessory()) return false; - var (changed, _) = _items.ResolveRestrictedGear(gear.Armor(), slot, race, gender); + var (changed, _) = _items.ResolveRestrictedGear(data.CurrentItem.Armor(), data.Slot, data.CurrentRace, data.CurrentGender); return changed; } - - public DataChange DrawEquip(EquipSlot slot, in DesignData designData, out EquipItem rArmor, out StainId rStain, EquipFlag? cApply, - out bool rApply, out bool rApplyStain, bool locked) - => DrawEquip(slot, designData.Item(slot), out rArmor, designData.Stain(slot), out rStain, cApply, out rApply, out rApplyStain, locked, - designData.Customize.Gender, designData.Customize.Race); - - public DataChange DrawEquip(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, EquipFlag? cApply, - out bool rApply, out bool rApplyStain, bool locked, Gender gender = Gender.Unknown, Race race = Race.Unknown) + public void DrawEquip(EquipDrawData equipDrawData) { if (_config.HideApplyCheckmarks) - cApply = null; + equipDrawData.DisplayApplication = false; - using var id = ImRaii.PushId((int)slot); + using var id = ImRaii.PushId((int)equipDrawData.Slot); var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); if (_config.SmallEquip) - return DrawEquipSmall(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain, locked, gender, race); - - if (!locked && _codes.EnabledArtisan) - return DrawEquipArtisan(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain); - - return DrawEquipNormal(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain, locked, gender, race); + DrawEquipSmall(equipDrawData); + else if (!equipDrawData.Locked && _codes.EnabledArtisan) + DrawEquipArtisan(equipDrawData); + else + DrawEquipNormal(equipDrawData); } - public DataChange DrawWeapons(in DesignData designData, out EquipItem rMainhand, out EquipItem rOffhand, out StainId rMainhandStain, - out StainId rOffhandStain, EquipFlag? cApply, bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain, - out bool rApplyOffhand, out bool rApplyOffhandStain, bool locked) - => DrawWeapons(designData.Item(EquipSlot.MainHand), out rMainhand, designData.Item(EquipSlot.OffHand), out rOffhand, - designData.Stain(EquipSlot.MainHand), out rMainhandStain, designData.Stain(EquipSlot.OffHand), out rOffhandStain, cApply, - allWeapons, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked); - - private DataChange DrawWeapons(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand, - StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply, - bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain, - bool locked) + public void DrawWeapons(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) { - if (cMainhand.ModelId.Id == 0) - { - rOffhand = cOffhand; - rMainhand = cMainhand; - rMainhandStain = cMainhandStain; - rOffhandStain = cOffhandStain; - rApplyMainhand = false; - rApplyMainhandStain = false; - rApplyOffhand = false; - rApplyOffhandStain = false; - return DataChange.None; - } + if (mainhand.CurrentItem.ModelId.Id == 0) + return; if (_config.HideApplyCheckmarks) - cApply = null; + { + mainhand.DisplayApplication = false; + offhand.DisplayApplication = false; + } using var id = ImRaii.PushId("Weapons"); var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); if (_config.SmallEquip) - return DrawWeaponsSmall(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain, - out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked, - allWeapons); - - if (!locked && _codes.EnabledArtisan) - return DrawWeaponsArtisan(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain, - out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain); - - return DrawWeaponsNormal(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain, - out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked, - allWeapons); + DrawWeaponsSmall(mainhand, offhand, allWeapons); + else if (!mainhand.Locked && _codes.EnabledArtisan) + DrawWeaponsArtisan(mainhand, offhand); + else + DrawWeaponsNormal(mainhand, offhand, allWeapons); } - public static bool DrawHatState(bool currentValue, out bool newValue, bool locked) - => UiHelpers.DrawCheckbox("Hat Visible", "Hide or show the characters head gear.", currentValue, out newValue, locked); - - public static DataChange DrawHatState(bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) - => UiHelpers.DrawMetaToggle("Hat Visible", currentValue, currentApply, out newValue, out newApply, locked); - - public static bool DrawVisorState(bool currentValue, out bool newValue, bool locked) - => UiHelpers.DrawCheckbox("Visor Toggled", "Toggle the visor state of the characters head gear.", currentValue, out newValue, locked); - - public static DataChange DrawVisorState(bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) - => UiHelpers.DrawMetaToggle("Visor Toggled", currentValue, currentApply, out newValue, out newApply, locked); - - public static bool DrawWeaponState(bool currentValue, out bool newValue, bool locked) - => UiHelpers.DrawCheckbox("Weapon Visible", "Hide or show the characters weapons when not drawn.", currentValue, out newValue, locked); - - public static DataChange DrawWeaponState(bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) - => UiHelpers.DrawMetaToggle("Weapon Visible", currentValue, currentApply, out newValue, out newApply, locked); - - private bool DrawMainhand(EquipItem current, bool drawAll, out EquipItem weapon, out string label, bool locked, bool small, bool open) + public static void DrawMetaToggle(in ToggleDrawData data) { - weapon = current; - if (!_weaponCombo.TryGetValue(drawAll ? FullEquipType.Unknown : current.Type, out var combo)) + if (data.DisplayApplication) { - label = string.Empty; - return false; + var (valueChanged, applyChanged) = UiHelpers.DrawMetaToggle(data.Label, data.CurrentValue, data.CurrentApply, out var newValue, + out var newApply, data.Locked); + if (valueChanged) + data.SetValue(newValue); + if (applyChanged) + data.SetApply(newApply); } - - label = combo.Label; - - var unknown = !_gPose.InGPose && current.Type is FullEquipType.Unknown; - var ret = false; - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); - using (var disabled = ImRaii.Disabled(locked | unknown)) + else { - if (!locked && open) - UiHelpers.OpenCombo($"##{label}"); - if (combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth)) - { - ret = true; - weapon = combo.CurrentSelection; - } + if (UiHelpers.DrawCheckbox(data.Label, data.Tooltip, data.CurrentValue, out var newValue, data.Locked)) + data.SetValue(newValue); } - - if (unknown && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip("The weapon type could not be identified, thus changing it to other weapons of that type is not possible."); - - return ret; - } - - private bool DrawOffhand(EquipItem mainhand, EquipItem current, out EquipItem weapon, out string label, bool locked, bool small, bool clear, - bool open) - { - weapon = current; - if (!_weaponCombo.TryGetValue(current.Type, out var combo)) - { - label = string.Empty; - return false; - } - - label = combo.Label; - locked |= !_gPose.InGPose && (current.Type is FullEquipType.Unknown || mainhand.Type is FullEquipType.Unknown); - using var disabled = ImRaii.Disabled(locked); - if (!locked && open) - UiHelpers.OpenCombo($"##{combo.Label}"); - var change = combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth); - if (change) - weapon = combo.CurrentSelection; - - if (!locked) - { - var defaultOffhand = _items.GetDefaultOffhand(mainhand); - if (defaultOffhand.Id != weapon.Id) - { - ImGuiUtil.HoverTooltip("Right-click to set to Default."); - if (clear || ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - change = true; - weapon = defaultOffhand; - } - } - } - - return change; - } - - private bool DrawApply(EquipSlot slot, EquipFlag flags, out bool enabled, bool locked) - => UiHelpers.DrawCheckbox($"##apply{slot}", "Apply this item when applying the Design.", flags.HasFlag(slot.ToFlag()), out enabled, - locked); - - private bool DrawApplyStain(EquipSlot slot, EquipFlag flags, out bool enabled, bool locked) - => UiHelpers.DrawCheckbox($"##applyStain{slot}", "Apply this dye when applying the Design.", flags.HasFlag(slot.ToStainFlag()), - out enabled, locked); - - private bool DrawItem(EquipSlot slot, EquipItem current, out EquipItem armor, out string label, bool locked, bool small, bool clear, - bool open) - { - Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(DrawItem)} on {slot}."); - var combo = _itemCombo[slot.ToIndex()]; - label = combo.Label; - armor = current; - if (!locked && open) - UiHelpers.OpenCombo($"##{combo.Label}"); - - using var disabled = ImRaii.Disabled(locked); - var change = combo.Draw(armor.Name, armor.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth); - if (change) - armor = combo.CurrentSelection; - - if (!locked && armor.ModelId.Id != 0) - { - if (clear || ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - change = true; - armor = ItemManager.NothingItem(slot); - } - - ImGuiUtil.HoverTooltip("Right-click to clear."); - } - - return change; } public bool DrawAllStain(out StainId ret, bool locked) @@ -303,50 +166,97 @@ public class EquipmentDrawer return change; } - private bool DrawStain(EquipSlot slot, StainId current, out StainId ret, bool locked, bool small) + #region Artisan + + private void DrawEquipArtisan(EquipDrawData data) { - var found = _stainData.TryGetValue(current, out var stain); - using var disabled = ImRaii.Disabled(locked); - var change = small - ? _stainCombo.Draw($"##stain{slot}", stain.RgbaColor, stain.Name, found, stain.Gloss) - : _stainCombo.Draw($"##stain{slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, _comboLength); - ret = current; - if (change) - if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) - ret = stain.RowIndex; - else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) - ret = Stain.None.RowIndex; + DrawStainArtisan(data); + ImGui.SameLine(); + DrawArmorArtisan(data); + if (!data.DisplayApplication) + return; - if (!locked && ret != Stain.None.RowIndex) + ImGui.SameLine(); + DrawApply(data); + ImGui.SameLine(); + DrawApplyStain(data); + } + + private void DrawWeaponsArtisan(in EquipDrawData mainhand, in EquipDrawData offhand) + { + using (var _ = ImRaii.PushId(0)) { - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - ret = Stain.None.RowIndex; - change = true; - } - - ImGuiUtil.HoverTooltip("Right-click to clear."); + DrawStainArtisan(mainhand); + ImGui.SameLine(); + DrawWeapon(mainhand); } - return change; + using (var _ = ImRaii.PushId(1)) + { + DrawStainArtisan(offhand); + ImGui.SameLine(); + DrawWeapon(offhand); + } + + return; + + void DrawWeapon(in EquipDrawData current) + { + int setId = current.CurrentItem.ModelId.Id; + int type = current.CurrentItem.WeaponType.Id; + int variant = current.CurrentItem.Variant.Id; + ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##setId", ref setId, 0, 0)) + { + var newSetId = (SetId)Math.Clamp(setId, 0, ushort.MaxValue); + if (newSetId.Id != current.CurrentItem.ModelId.Id) + current.ItemSetter(_items.Identify(current.Slot, newSetId, current.CurrentItem.WeaponType, current.CurrentItem.Variant)); + } + + ImGui.SameLine(); + ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##type", ref type, 0, 0)) + { + var newType = (WeaponType)Math.Clamp(type, 0, ushort.MaxValue); + if (newType.Id != current.CurrentItem.WeaponType.Id) + current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.ModelId, newType, current.CurrentItem.Variant)); + } + + ImGui.SameLine(); + ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##variant", ref variant, 0, 0)) + { + var newVariant = (Variant)Math.Clamp(variant, 0, byte.MaxValue); + if (newVariant.Id != current.CurrentItem.Variant.Id) + current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.ModelId, current.CurrentItem.WeaponType, newVariant)); + } + } + } + + /// Draw an input for stain that can set arbitrary values instead of choosing valid stains. + private static void DrawStainArtisan(EquipDrawData data) + { + int stainId = data.CurrentStain.Id; + ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); + if (!ImGui.InputInt("##stain", ref stainId, 0, 0)) + return; + + var newStainId = (StainId)Math.Clamp(stainId, 0, byte.MaxValue); + if (newStainId != data.CurrentStain.Id) + data.StainSetter(newStainId); } /// Draw an input for armor that can set arbitrary values instead of choosing items. - private bool DrawArmorArtisan(EquipSlot slot, EquipItem current, out EquipItem armor) + private void DrawArmorArtisan(EquipDrawData data) { - int setId = current.ModelId.Id; - int variant = current.Variant.Id; - var ret = false; - armor = current; + int setId = data.CurrentItem.ModelId.Id; + int variant = data.CurrentItem.Variant.Id; ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); if (ImGui.InputInt("##setId", ref setId, 0, 0)) { var newSetId = (SetId)Math.Clamp(setId, 0, ushort.MaxValue); - if (newSetId.Id != current.ModelId.Id) - { - armor = _items.Identify(slot, newSetId, current.Variant); - ret = true; - } + if (newSetId.Id != data.CurrentItem.ModelId.Id) + data.ItemSetter(_items.Identify(data.Slot, newSetId, data.CurrentItem.Variant)); } ImGui.SameLine(); @@ -354,141 +264,286 @@ public class EquipmentDrawer if (ImGui.InputInt("##variant", ref variant, 0, 0)) { var newVariant = (byte)Math.Clamp(variant, 0, byte.MaxValue); - if (newVariant != current.Variant) - { - armor = _items.Identify(slot, current.ModelId, newVariant); - ret = true; - } + if (newVariant != data.CurrentItem.Variant) + data.ItemSetter(_items.Identify(data.Slot, data.CurrentItem.ModelId, newVariant)); } - - return ret; } - /// Draw an input for stain that can set arbitrary values instead of choosing valid stains. - private bool DrawStainArtisan(EquipSlot slot, StainId current, out StainId stain) - { - int stainId = current.Id; - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##stain", ref stainId, 0, 0)) - { - var newStainId = (StainId)Math.Clamp(stainId, 0, byte.MaxValue); - if (newStainId != current) - { - stain = newStainId; - return true; - } - } + #endregion - stain = current; - return false; - } + #region Small - private DataChange DrawEquipArtisan(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, - EquipFlag? cApply, out bool rApply, out bool rApplyStain) + private void DrawEquipSmall(in EquipDrawData equipDrawData) { - var changes = DataChange.None; - if (DrawStainArtisan(slot, cStain, out rStain)) - changes |= DataChange.Stain; + DrawStain(equipDrawData, true); ImGui.SameLine(); - if (DrawArmorArtisan(slot, cArmor, out rArmor)) - changes |= DataChange.Item; - if (cApply.HasValue) + DrawItem(equipDrawData, out var label, true, false, false); + if (equipDrawData.DisplayApplication) { ImGui.SameLine(); - if (DrawApply(slot, cApply.Value, out rApply, false)) - changes |= DataChange.ApplyItem; + DrawApply(equipDrawData); ImGui.SameLine(); - if (DrawApplyStain(slot, cApply.Value, out rApplyStain, false)) - changes |= DataChange.ApplyStain; - } - else - { - rApply = false; - rApplyStain = false; + DrawApplyStain(equipDrawData); } - return changes; - } - - private DataChange DrawEquipSmall(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, - EquipFlag? cApply, out bool rApply, out bool rApplyStain, bool locked, Gender gender, Race race) - { - var changes = DataChange.None; - if (DrawStain(slot, cStain, out rStain, locked, true)) - changes |= DataChange.Stain; - ImGui.SameLine(); - if (DrawItem(slot, cArmor, out rArmor, out var label, locked, true, false, false)) - changes |= DataChange.Item; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(slot, cApply.Value, out rApply, false)) - changes |= DataChange.ApplyItem; - ImGui.SameLine(); - if (DrawApplyStain(slot, cApply.Value, out rApplyStain, false)) - changes |= DataChange.ApplyStain; - } - else - { - rApply = false; - rApplyStain = false; - } - - if (VerifyRestrictedGear(slot, rArmor, gender, race)) + if (VerifyRestrictedGear(equipDrawData)) label += " (Restricted)"; ImGui.SameLine(); ImGui.TextUnformatted(label); - - return changes; } - private DataChange DrawEquipNormal(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, - EquipFlag? cApply, out bool rApply, out bool rApplyStain, bool locked, Gender gender, Race race) + private void DrawWeaponsSmall(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) { - var changes = DataChange.None; - cArmor.DrawIcon(_textures, _iconSize, slot); + DrawStain(mainhand, true); + ImGui.SameLine(); + DrawMainhand(ref mainhand, ref offhand, out var mainhandLabel, allWeapons, true, false); + if (mainhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(mainhand); + ImGui.SameLine(); + DrawApplyStain(mainhand); + } + + if (allWeapons) + mainhandLabel += $" ({mainhand.CurrentItem.Type.ToName()})"; + WeaponHelpMarker(mainhandLabel); + + if (offhand.CurrentItem.Type is FullEquipType.Unknown) + return; + + DrawStain(offhand, true); + ImGui.SameLine(); + DrawOffhand(mainhand, offhand, out var offhandLabel, true, false, false); + if (offhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(offhand); + ImGui.SameLine(); + DrawApplyStain(offhand); + } + + WeaponHelpMarker(offhandLabel); + } + + #endregion + + #region Normal + + private void DrawEquipNormal(in EquipDrawData equipDrawData) + { + equipDrawData.CurrentItem.DrawIcon(_textures, _iconSize, equipDrawData.Slot); var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); ImGui.SameLine(); using var group = ImRaii.Group(); - if (DrawItem(slot, cArmor, out rArmor, out var label, locked, false, right, left)) - changes |= DataChange.Item; - if (cApply.HasValue) + DrawItem(equipDrawData, out var label, false, right, left); + if (equipDrawData.DisplayApplication) { ImGui.SameLine(); - if (DrawApply(slot, cApply.Value, out rApply, locked)) - changes |= DataChange.ApplyItem; - } - else - { - rApply = true; + DrawApply(equipDrawData); } ImGui.SameLine(); ImGui.TextUnformatted(label); - if (DrawStain(slot, cStain, out rStain, locked, false)) - changes |= DataChange.Stain; - if (cApply.HasValue) + DrawStain(equipDrawData, false); + if (equipDrawData.DisplayApplication) { ImGui.SameLine(); - if (DrawApplyStain(slot, cApply.Value, out rApplyStain, locked)) - changes |= DataChange.ApplyStain; - } - else - { - rApplyStain = true; + DrawApplyStain(equipDrawData); } - if (VerifyRestrictedGear(slot, rArmor, gender, race)) + if (VerifyRestrictedGear(equipDrawData)) { ImGui.SameLine(); ImGui.TextUnformatted("(Restricted)"); } - - return changes; } + private void DrawWeaponsNormal(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }); + + mainhand.CurrentItem.DrawIcon(_textures, _iconSize, EquipSlot.MainHand); + var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); + ImGui.SameLine(); + using (var group = ImRaii.Group()) + { + DrawMainhand(ref mainhand, ref offhand, out var mainhandLabel, allWeapons, false, left); + if (mainhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(mainhand); + } + + WeaponHelpMarker(mainhandLabel, allWeapons ? mainhand.CurrentItem.Type.ToName() : null); + + DrawStain(mainhand, false); + if (mainhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApplyStain(mainhand); + } + } + + if (offhand.CurrentItem.Type is FullEquipType.Unknown) + return; + + offhand.CurrentItem.DrawIcon(_textures, _iconSize, EquipSlot.OffHand); + var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); + left = ImGui.IsItemClicked(ImGuiMouseButton.Left); + ImGui.SameLine(); + using (var group = ImRaii.Group()) + { + DrawOffhand(mainhand, offhand, out var offhandLabel, false, right, left); + if (offhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(offhand); + } + + WeaponHelpMarker(offhandLabel); + + DrawStain(offhand, false); + if (offhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApplyStain(offhand); + } + } + } + + private void DrawStain(in EquipDrawData data, bool small) + { + var found = _stainData.TryGetValue(data.CurrentStain, out var stain); + using var disabled = ImRaii.Disabled(data.Locked); + var change = small + ? _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss) + : _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, _comboLength); + if (change) + if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) + data.StainSetter(stain.RowIndex); + else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) + data.StainSetter(Stain.None.RowIndex); + + if (!data.Locked && data.CurrentStain != Stain.None.RowIndex) + { + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + data.StainSetter(Stain.None.RowIndex); + + ImGuiUtil.HoverTooltip("Right-click to clear."); + } + } + + private void DrawItem(in EquipDrawData data, out string label, bool small, bool clear, bool open) + { + Debug.Assert(data.Slot.IsEquipment() || data.Slot.IsAccessory(), $"Called {nameof(DrawItem)} on {data.Slot}."); + + var combo = _itemCombo[data.Slot.ToIndex()]; + label = combo.Label; + if (!data.Locked && open) + UiHelpers.OpenCombo($"##{combo.Label}"); + + using var disabled = ImRaii.Disabled(data.Locked); + var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, + _requiredComboWidth); + if (change) + data.ItemSetter(combo.CurrentSelection); + else if (combo.CustomVariant.Id > 0) + data.ItemSetter(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant)); + + if (!data.Locked && data.CurrentItem.ModelId.Id != 0) + { + if (clear || ImGui.IsItemClicked(ImGuiMouseButton.Right)) + data.ItemSetter(ItemManager.NothingItem(data.Slot)); + + ImGuiUtil.HoverTooltip("Right-click to clear."); + } + } + + private void DrawMainhand(ref EquipDrawData mainhand, ref EquipDrawData offhand, out string label, bool drawAll, bool small, + bool open) + { + if (!_weaponCombo.TryGetValue(drawAll ? FullEquipType.Unknown : mainhand.CurrentItem.Type, out var combo)) + { + label = string.Empty; + return; + } + + label = combo.Label; + var unknown = !_gPose.InGPose && mainhand.CurrentItem.Type is FullEquipType.Unknown; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + using (var _ = ImRaii.Disabled(mainhand.Locked | unknown)) + { + if (!mainhand.Locked && open) + UiHelpers.OpenCombo($"##{label}"); + if (combo.Draw(mainhand.CurrentItem.Name, mainhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, + _requiredComboWidth)) + { + mainhand.ItemSetter(combo.CurrentSelection); + if (combo.CurrentSelection.Type.ValidOffhand() != mainhand.CurrentItem.Type.ValidOffhand()) + { + offhand.CurrentItem = _items.GetDefaultOffhand(combo.CurrentSelection); + offhand.ItemSetter(offhand.CurrentItem); + } + + mainhand.CurrentItem = combo.CurrentSelection; + } + } + + if (unknown && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("The weapon type could not be identified, thus changing it to other weapons of that type is not possible."); + } + + private void DrawOffhand(in EquipDrawData mainhand, in EquipDrawData offhand, out string label, bool small, bool clear, bool open) + { + if (!_weaponCombo.TryGetValue(offhand.CurrentItem.Type, out var combo)) + { + label = string.Empty; + return; + } + + label = combo.Label; + var locked = offhand.Locked + || !_gPose.InGPose && (offhand.CurrentItem.Type is FullEquipType.Unknown || mainhand.CurrentItem.Type is FullEquipType.Unknown); + using var disabled = ImRaii.Disabled(locked); + if (!locked && open) + UiHelpers.OpenCombo($"##{combo.Label}"); + if (combo.Draw(offhand.CurrentItem.Name, offhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, + _requiredComboWidth)) + offhand.ItemSetter(combo.CurrentSelection); + + if (locked) + return; + + var defaultOffhand = _items.GetDefaultOffhand(mainhand.CurrentItem); + if (defaultOffhand.Id == offhand.CurrentItem.Id) + return; + + ImGuiUtil.HoverTooltip("Right-click to set to Default."); + if (clear || ImGui.IsItemClicked(ImGuiMouseButton.Right)) + offhand.ItemSetter(defaultOffhand); + } + + private static void DrawApply(in EquipDrawData data) + { + if (UiHelpers.DrawCheckbox($"##apply{data.Slot}", "Apply this item when applying the Design.", data.CurrentApply, out var enabled, + data.Locked)) + data.ApplySetter(enabled); + } + + private static void DrawApplyStain(in EquipDrawData data) + { + if (UiHelpers.DrawCheckbox($"##applyStain{data.Slot}", "Apply this item when applying the Design.", data.CurrentApplyStain, + out var enabled, + data.Locked)) + data.ApplyStainSetter(enabled); + } + + #endregion + private static void WeaponHelpMarker(string label, string? type = null) { ImGui.SameLine(); @@ -497,261 +552,11 @@ public class EquipmentDrawer + "thus it is only allowed to change weapons to other weapons of the same type."); ImGui.SameLine(); ImGui.TextUnformatted(label); - if (type != null) - { - var pos = ImGui.GetItemRectMin(); - pos.Y += ImGui.GetFrameHeightWithSpacing(); - ImGui.GetWindowDrawList().AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), $"({type})"); - } - } + if (type == null) + return; - private DataChange DrawWeaponsSmall(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand, - StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply, - out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain, bool locked, - bool allWeapons) - { - var changes = DataChange.None; - if (DrawStain(EquipSlot.MainHand, cMainhandStain, out rMainhandStain, locked, true)) - changes |= DataChange.Stain; - ImGui.SameLine(); - - rOffhand = cOffhand; - if (DrawMainhand(cMainhand, allWeapons, out rMainhand, out var mainhandLabel, locked, true, false)) - { - changes |= DataChange.Item; - if (rMainhand.Type.ValidOffhand() != cMainhand.Type.ValidOffhand()) - { - rOffhand = _items.GetDefaultOffhand(rMainhand); - changes |= DataChange.Item2; - } - } - - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(EquipSlot.MainHand, cApply.Value, out rApplyMainhand, locked)) - changes |= DataChange.ApplyItem; - ImGui.SameLine(); - if (DrawApplyStain(EquipSlot.MainHand, cApply.Value, out rApplyMainhandStain, locked)) - changes |= DataChange.ApplyStain; - } - else - { - rApplyMainhand = true; - rApplyMainhandStain = true; - } - - if (allWeapons) - mainhandLabel += $" ({cMainhand.Type.ToName()})"; - WeaponHelpMarker(mainhandLabel); - - if (rOffhand.Type is FullEquipType.Unknown) - { - rOffhandStain = cOffhandStain; - rApplyOffhand = false; - rApplyOffhandStain = false; - return changes; - } - - if (DrawStain(EquipSlot.OffHand, cOffhandStain, out rOffhandStain, locked, true)) - changes |= DataChange.Stain2; - - ImGui.SameLine(); - if (DrawOffhand(rMainhand, rOffhand, out rOffhand, out var offhandLabel, locked, true, false, false)) - changes |= DataChange.Item2; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(EquipSlot.OffHand, cApply.Value, out rApplyOffhand, locked)) - changes |= DataChange.ApplyItem2; - ImGui.SameLine(); - if (DrawApplyStain(EquipSlot.OffHand, cApply.Value, out rApplyOffhandStain, locked)) - changes |= DataChange.ApplyStain2; - } - else - { - rApplyOffhand = true; - rApplyOffhandStain = true; - } - - WeaponHelpMarker(offhandLabel); - - return changes; - } - - private DataChange DrawWeaponsNormal(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand, - StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply, - out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain, bool locked, - bool allWeapons) - { - var changes = DataChange.None; - - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }); - - cMainhand.DrawIcon(_textures, _iconSize, EquipSlot.MainHand); - var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); - ImGui.SameLine(); - using (var group = ImRaii.Group()) - { - rOffhand = cOffhand; - if (DrawMainhand(cMainhand, allWeapons, out rMainhand, out var mainhandLabel, locked, false, left)) - { - changes |= DataChange.Item; - if (rMainhand.Type.ValidOffhand() != cMainhand.Type.ValidOffhand()) - { - rOffhand = _items.GetDefaultOffhand(rMainhand); - changes |= DataChange.Item2; - } - } - - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(EquipSlot.MainHand, cApply.Value, out rApplyMainhand, locked)) - changes |= DataChange.ApplyItem; - } - else - { - rApplyMainhand = true; - } - - WeaponHelpMarker(mainhandLabel, allWeapons ? cMainhand.Type.ToName() : null); - - if (DrawStain(EquipSlot.MainHand, cMainhandStain, out rMainhandStain, locked, false)) - changes |= DataChange.Stain; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApplyStain(EquipSlot.MainHand, cApply.Value, out rApplyMainhandStain, locked)) - changes |= DataChange.ApplyStain; - } - else - { - rApplyMainhandStain = true; - } - } - - if (rOffhand.Type is FullEquipType.Unknown) - { - rOffhandStain = cOffhandStain; - rApplyOffhand = false; - rApplyOffhandStain = false; - return changes; - } - - rOffhand.DrawIcon(_textures, _iconSize, EquipSlot.OffHand); - var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); - left = ImGui.IsItemClicked(ImGuiMouseButton.Left); - ImGui.SameLine(); - using (var group = ImRaii.Group()) - { - if (DrawOffhand(rMainhand, rOffhand, out rOffhand, out var offhandLabel, locked, false, right, left)) - changes |= DataChange.Item2; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(EquipSlot.OffHand, cApply.Value, out rApplyOffhand, locked)) - changes |= DataChange.ApplyItem2; - } - else - { - rApplyOffhand = true; - } - - WeaponHelpMarker(offhandLabel); - - if (DrawStain(EquipSlot.OffHand, cOffhandStain, out rOffhandStain, locked, false)) - changes |= DataChange.Stain2; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApplyStain(EquipSlot.OffHand, cApply.Value, out rApplyOffhandStain, locked)) - changes |= DataChange.ApplyStain2; - } - else - { - rApplyOffhandStain = true; - } - } - - return changes; - } - - private DataChange DrawWeaponsArtisan(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand, - StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply, - out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain) - { - rApplyMainhand = (cApply ?? 0).HasFlag(EquipFlag.Mainhand); - rApplyMainhandStain = (cApply ?? 0).HasFlag(EquipFlag.MainhandStain); - rApplyOffhand = (cApply ?? 0).HasFlag(EquipFlag.Offhand); - rApplyOffhandStain = (cApply ?? 0).HasFlag(EquipFlag.MainhandStain); - - bool DrawWeapon(EquipItem current, out EquipItem ret) - { - int setId = current.ModelId.Id; - int type = current.WeaponType.Id; - int variant = current.Variant.Id; - ret = current; - var changed = false; - - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##setId", ref setId, 0, 0)) - { - var newSetId = (SetId)Math.Clamp(setId, 0, ushort.MaxValue); - if (newSetId.Id != current.ModelId.Id) - { - ret = _items.Identify(EquipSlot.MainHand, newSetId, current.WeaponType, current.Variant); - changed = true; - } - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##type", ref type, 0, 0)) - { - var newType = (WeaponType)Math.Clamp(type, 0, ushort.MaxValue); - if (newType.Id != current.WeaponType.Id) - { - ret = _items.Identify(EquipSlot.MainHand, current.ModelId, newType, current.Variant); - changed = true; - } - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##variant", ref variant, 0, 0)) - { - var newVariant = (Variant)Math.Clamp(variant, 0, byte.MaxValue); - if (newVariant.Id != current.Variant.Id) - { - ret = _items.Identify(EquipSlot.MainHand, current.ModelId, current.WeaponType, newVariant); - changed = true; - } - } - - return changed; - } - - var ret = DataChange.None; - using (var id = ImRaii.PushId(0)) - { - if (DrawStainArtisan(EquipSlot.MainHand, cMainhandStain, out rMainhandStain)) - ret |= DataChange.Stain; - ImGui.SameLine(); - if (DrawWeapon(cMainhand, out rMainhand)) - ret |= DataChange.Item; - } - - using (var id = ImRaii.PushId(1)) - { - if (DrawStainArtisan(EquipSlot.OffHand, cOffhandStain, out rOffhandStain)) - ret |= DataChange.Stain; - ImGui.SameLine(); - if (DrawWeapon(cOffhand, out rOffhand)) - ret |= DataChange.Item; - } - - return ret; + var pos = ImGui.GetItemRectMin(); + pos.Y += ImGui.GetFrameHeightWithSpacing(); + ImGui.GetWindowDrawList().AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), $"({type})"); } } diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/Glamourer/Gui/Equipment/ItemCombo.cs index 5062949..4ddbbaa 100644 --- a/Glamourer/Gui/Equipment/ItemCombo.cs +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Dalamud.Plugin.Services; using Glamourer.Services; @@ -22,6 +23,9 @@ public sealed class ItemCombo : FilterComboCache private ItemId _currentItem; private float _innerWidth; + public SetId CustomSetId { get; private set; } + public Variant CustomVariant { get; private set; } + public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites) : base(() => GetItems(favorites, items, slot), log) { @@ -50,8 +54,9 @@ public sealed class ItemCombo : FilterComboCache public bool Draw(string previewName, ItemId previewIdx, float width, float innerWidth) { - _innerWidth = innerWidth; - _currentItem = previewIdx; + _innerWidth = innerWidth; + _currentItem = previewIdx; + CustomVariant = 0; return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); } @@ -117,4 +122,18 @@ public sealed class ItemCombo : FilterComboCache enumerable = enumerable.Append(ItemManager.SmallClothesItem(slot)); return enumerable.OrderByDescending(favorites.Contains).ThenBy(i => i.Name).Prepend(nothing).ToList(); } + + protected override void OnClosePopup() + { + // If holding control while the popup closes, try to parse the input as a full pair of set id and variant, and set a custom item for that. + if (!ImGui.GetIO().KeyCtrl) + return; + + var split = Filter.Text.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length != 2 || !ushort.TryParse(split[0], out var setId) || !byte.TryParse(split[1], out var variant)) + return; + + CustomSetId = setId; + CustomVariant = variant; + } } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index d049eec..a294b08 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -15,6 +15,7 @@ using Glamourer.Interop; using Glamourer.Interop.Structs; using Glamourer.Services; using Glamourer.State; +using Glamourer.Structs; using ImGuiNET; using OtterGui; using OtterGui.Classes; @@ -24,21 +25,11 @@ using Penumbra.GameData.Enums; namespace Glamourer.Gui.Tabs.ActorTab; -public class ActorPanel +public class ActorPanel(ActorSelector _selector, StateManager _stateManager, CustomizationDrawer _customizationDrawer, + EquipmentDrawer _equipmentDrawer, IdentifierService _identification, AutoDesignApplier _autoDesignApplier, + Configuration _config, DesignConverter _converter, ObjectManager _objects, DesignManager _designManager, ImportService _importService, + ICondition _conditions) { - private readonly ActorSelector _selector; - private readonly StateManager _stateManager; - private readonly CustomizationDrawer _customizationDrawer; - private readonly EquipmentDrawer _equipmentDrawer; - private readonly IdentifierService _identification; - private readonly AutoDesignApplier _autoDesignApplier; - private readonly Configuration _config; - private readonly DesignConverter _converter; - private readonly ObjectManager _objects; - private readonly DesignManager _designManager; - private readonly ImportService _importService; - private readonly ICondition _conditions; - private ActorIdentifier _identifier; private string _actorName = string.Empty; private Actor _actor = Actor.Null; @@ -46,29 +37,10 @@ public class ActorPanel private ActorState? _state; private bool _lockedRedraw; - public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer, - EquipmentDrawer equipmentDrawer, IdentifierService identification, AutoDesignApplier autoDesignApplier, - Configuration config, DesignConverter converter, ObjectManager objects, DesignManager designManager, ImportService importService, - ICondition conditions) - { - _selector = selector; - _stateManager = stateManager; - _customizationDrawer = customizationDrawer; - _equipmentDrawer = equipmentDrawer; - _identification = identification; - _autoDesignApplier = autoDesignApplier; - _config = config; - _converter = converter; - _objects = objects; - _designManager = designManager; - _importService = importService; - _conditions = conditions; - } - private CustomizeFlag CustomizeApplicationFlags => _lockedRedraw ? CustomizeFlagExtensions.AllRelevant & ~CustomizeFlagExtensions.RedrawRequired : CustomizeFlagExtensions.AllRelevant; - public unsafe void Draw() + public void Draw() { using var group = ImRaii.Group(); (_identifier, _data) = _selector.Selection; @@ -161,8 +133,7 @@ public class ActorPanel if (_customizationDrawer.Draw(_state!.ModelData.Customize, _state.IsLocked, _lockedRedraw)) _stateManager.ChangeCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, StateChanged.Source.Manual); - if (_customizationDrawer.DrawWetnessState(_state!.ModelData.IsWet(), out var newWetness, _state.IsLocked)) - _stateManager.ChangeWetness(_state, newWetness, StateChanged.Source.Manual); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.Wetness, _stateManager, _state)); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } @@ -176,63 +147,42 @@ public class ActorPanel var usedAllStain = _equipmentDrawer.DrawAllStain(out var newAllStain, _state!.IsLocked); foreach (var slot in EquipSlotExtensions.EqdpSlots) { - var changes = _equipmentDrawer.DrawEquip(slot, _state!.ModelData, out var newArmor, out var newStain, null, out _, out _, - _state.IsLocked); + var data = EquipDrawData.FromState(_stateManager, _state!, slot); + _equipmentDrawer.DrawEquip(data); if (usedAllStain) - { - changes |= DataChange.Stain; - newStain = newAllStain; - } - - switch (changes) - { - case DataChange.Item: - _stateManager.ChangeItem(_state, slot, newArmor, StateChanged.Source.Manual); - break; - case DataChange.Stain: - _stateManager.ChangeStain(_state, slot, newStain, StateChanged.Source.Manual); - break; - case DataChange.Item | DataChange.Stain: - _stateManager.ChangeEquip(_state, slot, newArmor, newStain, StateChanged.Source.Manual); - break; - } + _stateManager.ChangeStain(_state, slot, newAllStain, StateChanged.Source.Manual); } - var weaponChanges = _equipmentDrawer.DrawWeapons(_state!.ModelData, out var newMainhand, out var newOffhand, out var newMainhandStain, - out var newOffhandStain, null, GameMain.IsInGPose(), out _, out _, out _, out _, _state.IsLocked); - if (usedAllStain) + var mainhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.MainHand); + var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand); + _equipmentDrawer.DrawWeapons(mainhand, offhand, GameMain.IsInGPose()); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawEquipmentMetaToggles(); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + } + + private void DrawEquipmentMetaToggles() + { + using (var _ = ImRaii.Group()) { - weaponChanges |= DataChange.Stain | DataChange.Stain2; - newMainhandStain = newAllStain; - newOffhandStain = newAllStain; + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.HatState, _stateManager, _state!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Head, _stateManager, _state!)); } - if (weaponChanges.HasFlag(DataChange.Item)) - if (weaponChanges.HasFlag(DataChange.Stain)) - _stateManager.ChangeEquip(_state, EquipSlot.MainHand, newMainhand, newMainhandStain, StateChanged.Source.Manual); - else - _stateManager.ChangeItem(_state, EquipSlot.MainHand, newMainhand, StateChanged.Source.Manual); - else if (weaponChanges.HasFlag(DataChange.Stain)) - _stateManager.ChangeStain(_state, EquipSlot.MainHand, newMainhandStain, StateChanged.Source.Manual); - - if (weaponChanges.HasFlag(DataChange.Item2)) - if (weaponChanges.HasFlag(DataChange.Stain2)) - _stateManager.ChangeEquip(_state, EquipSlot.OffHand, newOffhand, newOffhandStain, StateChanged.Source.Manual); - else - _stateManager.ChangeItem(_state, EquipSlot.OffHand, newOffhand, StateChanged.Source.Manual); - else if (weaponChanges.HasFlag(DataChange.Stain2)) - _stateManager.ChangeStain(_state, EquipSlot.OffHand, newOffhandStain, StateChanged.Source.Manual); - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (EquipmentDrawer.DrawHatState(_state!.ModelData.IsHatVisible(), out var newHatState, _state!.IsLocked)) - _stateManager.ChangeHatState(_state, newHatState, StateChanged.Source.Manual); ImGui.SameLine(); - if (EquipmentDrawer.DrawVisorState(_state!.ModelData.IsVisorToggled(), out var newVisorState, _state!.IsLocked)) - _stateManager.ChangeVisorState(_state, newVisorState, StateChanged.Source.Manual); + using (var _ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.VisorState, _stateManager, _state!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Body, _stateManager, _state!)); + } + ImGui.SameLine(); - if (EquipmentDrawer.DrawWeaponState(_state!.ModelData.IsWeaponVisible(), out var newWeaponState, _state!.IsLocked)) - _stateManager.ChangeWeaponState(_state, newWeaponState, StateChanged.Source.Manual); - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + using (var _ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.WeaponState, _stateManager, _state!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!)); + } } private void DrawMonsterPanel() @@ -346,8 +296,8 @@ public class ActorPanel { ImGui.OpenPopup("Save as Design"); _newName = _state!.Identifier.ToName(); - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); - _newDesign = _converter.Convert(_state, applyGear, applyCustomize); + var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags(); + _newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest); } private void SaveDesignDrawPopup() @@ -382,8 +332,8 @@ public class ActorPanel { try { - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); - var text = _converter.ShareBase64(_state!, applyGear, applyCustomize); + var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags(); + var text = _converter.ShareBase64(_state!, applyGear, applyCustomize, applyCrest); ImGui.SetClipboardText(text); } catch (Exception ex) @@ -422,9 +372,9 @@ public class ActorPanel !data.Valid || id == _identifier || _state!.ModelData.ModelId != 0)) return; - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); + var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags(); if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) - _stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize), state, + _stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize, applyCrest), state, StateChanged.Source.Manual); } @@ -440,9 +390,9 @@ public class ActorPanel !data.Valid || id == _identifier || _state!.ModelData.ModelId != 0)) return; - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); + var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags(); if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) - _stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize), state, + _stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize, applyCrest), state, StateChanged.Source.Manual); } } diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index 3a0f437..6b0e301 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; @@ -29,17 +30,18 @@ public class SetPanel private readonly CustomizeUnlockManager _customizeUnlocks; private readonly CustomizationService _customizations; - private readonly Configuration _config; - private readonly RevertDesignCombo _designCombo; - private readonly JobGroupCombo _jobGroupCombo; - private readonly IdentifierDrawer _identifierDrawer; + private readonly Configuration _config; + private readonly RevertDesignCombo _designCombo; + private readonly JobGroupCombo _jobGroupCombo; + private readonly IdentifierDrawer _identifierDrawer; private string? _tempName; private int _dragIndex = -1; private Action? _endAction; - public SetPanel(SetSelector selector, AutoDesignManager manager, JobService jobs, ItemUnlockManager itemUnlocks, RevertDesignCombo designCombo, + public SetPanel(SetSelector selector, AutoDesignManager manager, JobService jobs, ItemUnlockManager itemUnlocks, + RevertDesignCombo designCombo, CustomizeUnlockManager customizeUnlocks, CustomizationService customizations, IdentifierDrawer identifierDrawer, Configuration config) { _selector = selector; @@ -216,11 +218,11 @@ public class SetPanel ImGui.TableNextColumn(); DrawApplicationTypeBoxes(Selection, design, idx, singleRow); ImGui.TableNextColumn(); - _jobGroupCombo.Draw(Selection, design, idx); + DrawConditions(design, idx); } else { - _jobGroupCombo.Draw(Selection, design, idx); + DrawConditions(design, idx); ImGui.TableNextColumn(); DrawApplicationTypeBoxes(Selection, design, idx, singleRow); } @@ -244,6 +246,38 @@ public class SetPanel _endAction = null; } + private int _tmpGearset = int.MaxValue; + + private void DrawConditions(AutoDesign design, int idx) + { + var usingGearset = design.GearsetIndex >= 0; + if (ImGui.Button($"{(usingGearset ? "Gearset:" : "Jobs:")}##usingGearset")) + { + usingGearset = !usingGearset; + _manager.ChangeGearsetCondition(Selection, idx, (short)(usingGearset ? 0 : -1)); + } + + ImGuiUtil.HoverTooltip("Click to switch between Job and Gearset restrictions."); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + if (usingGearset) + { + var set = 1 + (_tmpGearset == int.MaxValue ? design.GearsetIndex : _tmpGearset); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputInt("##whichGearset", ref set, 0, 0)) + _tmpGearset = Math.Clamp(set, 1, 100); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + _manager.ChangeGearsetCondition(Selection, idx, (short)(_tmpGearset - 1)); + _tmpGearset = int.MaxValue; + } + } + else + { + _jobGroupCombo.Draw(Selection, design, idx); + } + } + private void DrawWarnings(AutoDesign design, int idx) { if (design.Revert) @@ -252,7 +286,7 @@ public class SetPanel var size = new Vector2(ImGui.GetFrameHeight()); size.X += ImGuiHelpers.GlobalScale; - var (equipFlags, customizeFlags, _, _, _, _) = design.ApplyWhat(); + var (equipFlags, customizeFlags, _, _, _, _, _) = design.ApplyWhat(); var sb = new StringBuilder(); foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)) { @@ -423,7 +457,7 @@ public class SetPanel "Apply all customization changes that are enabled in this design and that are valid in a fixed design and for the given race and gender."), (AutoDesign.Type.Armor, "Apply all armor piece changes that are enabled in this design and that are valid in a fixed design."), (AutoDesign.Type.Accessories, "Apply all accessory changes that are enabled in this design and that are valid in a fixed design."), - (AutoDesign.Type.Stains, "Apply all dye changes that are enabled in this design."), + (AutoDesign.Type.GearCustomization, "Apply all dye and crest changes that are enabled in this design."), (AutoDesign.Type.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."), }; diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 7d946fb..ad1d46f 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using System.Text; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; @@ -21,6 +22,7 @@ using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; using Glamourer.State; +using Glamourer.Structs; using Glamourer.Unlocks; using Glamourer.Utility; using ImGuiNET; @@ -43,6 +45,7 @@ public unsafe class DebugTab : ITab private readonly VisorService _visorService; private readonly ChangeCustomizeService _changeCustomizeService; private readonly UpdateSlotService _updateSlotService; + private readonly CrestService _crestService; private readonly WeaponService _weaponService; private readonly MetaService _metaService; private readonly InventoryService _inventoryService; @@ -50,7 +53,7 @@ public unsafe class DebugTab : ITab private readonly ObjectManager _objectManager; private readonly GlamourerIpc _ipc; private readonly CodeService _code; - private readonly ImportService _importService; + private readonly ImportService _importService; private readonly ItemManager _items; private readonly ActorService _actors; @@ -82,7 +85,7 @@ public unsafe class DebugTab : ITab PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface, AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks, ItemUnlockManager itemUnlocks, DesignConverter designConverter, ImportService importService, InventoryService inventoryService, - HumanModelList humans, FunModule funModule) + HumanModelList humans, FunModule funModule, CrestService crestService) { _changeCustomizeService = changeCustomizeService; _visorService = visorService; @@ -107,10 +110,11 @@ public unsafe class DebugTab : ITab _customizeUnlocks = customizeUnlocks; _itemUnlocks = itemUnlocks; _designConverter = designConverter; - _importService = importService; + _importService = importService; _inventoryService = inventoryService; _humans = humans; _funModule = funModule; + _crestService = crestService; } public ReadOnlySpan Label @@ -200,6 +204,7 @@ public unsafe class DebugTab : ITab DrawWetness(actor, model); DrawEquip(actor, model); DrawCustomize(actor, model); + DrawCrests(actor, model); } private string _objectFilter = string.Empty; @@ -477,6 +482,32 @@ public unsafe class DebugTab : ITab } } + private void DrawCrests(Actor actor, Model model) + { + using var id = ImRaii.PushId("Crests"); + CrestFlag whichToggle = 0; + CrestFlag totalModelFlags = 0; + foreach (var crestFlag in CrestExtensions.AllRelevantSet) + { + id.Push((int)crestFlag); + var modelCrest = CrestService.GetModelCrest(actor, crestFlag); + if (modelCrest) + totalModelFlags |= crestFlag; + ImGuiUtil.DrawTableColumn($"{crestFlag.ToLabel()} Crest"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetCrest(crestFlag).ToString() : "No Character"); + ImGuiUtil.DrawTableColumn(modelCrest.ToString()); + + ImGui.TableNextColumn(); + if (model.IsHuman && ImGui.SmallButton("Toggle")) + whichToggle = crestFlag; + + id.Pop(); + } + + if (whichToggle != 0) + _crestService.UpdateCrests(actor, totalModelFlags ^ whichToggle); + } + #endregion #region Penumbra diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs index b323b63..b288303 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs @@ -118,6 +118,7 @@ public sealed class DesignFileSystemSelector : FileSystemSelector Framework.Instance()->UserPath; } diff --git a/Glamourer/Gui/ToggleDrawData.cs b/Glamourer/Gui/ToggleDrawData.cs new file mode 100644 index 0000000..dda4584 --- /dev/null +++ b/Glamourer/Gui/ToggleDrawData.cs @@ -0,0 +1,104 @@ +using System; +using Glamourer.Designs; +using Glamourer.Events; +using Glamourer.State; +using Glamourer.Structs; +using Penumbra.GameData.Enums; + +namespace Glamourer.Gui; + +public ref struct ToggleDrawData +{ + public bool Locked; + public bool DisplayApplication; + + public bool CurrentValue; + public bool CurrentApply; + + public Action SetValue = null!; + public Action SetApply = null!; + + public string Label = string.Empty; + public string Tooltip = string.Empty; + + public ToggleDrawData() + { } + + public static ToggleDrawData FromDesign(ActorState.MetaIndex index, DesignManager manager, Design design) + { + var (label, value, apply, setValue, setApply) = index switch + { + ActorState.MetaIndex.HatState => ("Hat Visible", design.DesignData.IsHatVisible(), design.DoApplyHatVisible(), + (Action)(b => manager.ChangeMeta(design, index, b)), (Action)(b => manager.ChangeApplyMeta(design, index, b))), + ActorState.MetaIndex.VisorState => ("Visor Toggled", design.DesignData.IsVisorToggled(), design.DoApplyVisorToggle(), + b => manager.ChangeMeta(design, index, b), b => manager.ChangeApplyMeta(design, index, b)), + ActorState.MetaIndex.WeaponState => ("Weapon Visible", design.DesignData.IsWeaponVisible(), design.DoApplyWeaponVisible(), + b => manager.ChangeMeta(design, index, b), b => manager.ChangeApplyMeta(design, index, b)), + ActorState.MetaIndex.Wetness => ("Force Wetness", design.DesignData.IsWet(), design.DoApplyWetness(), + b => manager.ChangeMeta(design, index, b), b => manager.ChangeApplyMeta(design, index, b)), + _ => throw new Exception("Unsupported meta index."), + }; + + return new ToggleDrawData + { + Label = label, + Tooltip = string.Empty, + Locked = design.WriteProtected(), + DisplayApplication = true, + CurrentValue = value, + CurrentApply = apply, + SetValue = setValue, + SetApply = setApply, + }; + } + + public static ToggleDrawData CrestFromDesign(CrestFlag slot, DesignManager manager, Design design) + => new() + { + Label = $"{slot.ToLabel()} Crest", + Tooltip = string.Empty, + Locked = design.WriteProtected(), + DisplayApplication = true, + CurrentValue = design.DesignData.Crest(slot), + CurrentApply = design.DoApplyCrest(slot), + SetValue = v => manager.ChangeCrest(design, slot, v), + SetApply = v => manager.ChangeApplyCrest(design, slot, v), + }; + + public static ToggleDrawData CrestFromState(CrestFlag slot, StateManager manager, ActorState state) + => new() + { + Label = $"{slot.ToLabel()} Crest", + Tooltip = "Hide or show your free company crest on this piece of gear.", + Locked = state.IsLocked, + CurrentValue = state.ModelData.Crest(slot), + SetValue = v => manager.ChangeCrest(state, slot, v, StateChanged.Source.Manual), + }; + + public static ToggleDrawData FromState(ActorState.MetaIndex index, StateManager manager, ActorState state) + { + var (label, tooltip, value, setValue) = index switch + { + ActorState.MetaIndex.HatState => ("Hat Visible", "Hide or show the characters head gear.", state.ModelData.IsHatVisible(), + (Action)(b => manager.ChangeHatState(state, b, StateChanged.Source.Manual))), + ActorState.MetaIndex.VisorState => ("Visor Toggled", "Toggle the visor state of the characters head gear.", + state.ModelData.IsVisorToggled(), + b => manager.ChangeVisorState(state, b, StateChanged.Source.Manual)), + ActorState.MetaIndex.WeaponState => ("Weapon Visible", "Hide or show the characters weapons when not drawn.", + state.ModelData.IsWeaponVisible(), + b => manager.ChangeWeaponState(state, b, StateChanged.Source.Manual)), + ActorState.MetaIndex.Wetness => ("Force Wetness", "Force the character to be wet or not.", state.ModelData.IsWet(), + b => manager.ChangeWetness(state, b, StateChanged.Source.Manual)), + _ => throw new Exception("Unsupported meta index."), + }; + + return new ToggleDrawData + { + Label = label, + Tooltip = tooltip, + Locked = state.IsLocked, + CurrentValue = value, + SetValue = setValue, + }; + } +} diff --git a/Glamourer/Gui/UiHelpers.cs b/Glamourer/Gui/UiHelpers.cs index eb1f1c2..6e83838 100644 --- a/Glamourer/Gui/UiHelpers.cs +++ b/Glamourer/Gui/UiHelpers.cs @@ -1,4 +1,3 @@ -using System; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Utility; @@ -9,27 +8,13 @@ using Glamourer.Unlocks; using ImGuiNET; using Lumina.Misc; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.Gui; -[Flags] -public enum DataChange : byte -{ - None = 0x00, - Item = 0x01, - Stain = 0x02, - ApplyItem = 0x04, - ApplyStain = 0x08, - Item2 = 0x10, - Stain2 = 0x20, - ApplyItem2 = 0x40, - ApplyStain2 = 0x80, -} - public static class UiHelpers { /// Open a combo popup with another method than the combo itself. @@ -71,14 +56,15 @@ public static class UiHelpers return ret; } - public static DataChange DrawMetaToggle(string label, bool currentValue, bool currentApply, out bool newValue, + public static (bool, bool) DrawMetaToggle(string label, bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) { var flags = (sbyte)(currentApply ? currentValue ? 1 : -1 : 0); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); using (var disabled = ImRaii.Disabled(locked)) { - if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw("##" + label, flags, out flags)) + if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw( + "##" + label, flags, out flags)) { (newValue, newApply) = flags switch { @@ -99,22 +85,16 @@ public static class UiHelpers ImGui.SameLine(); ImGui.TextUnformatted(label); - return (currentApply != newApply, currentValue != newValue) switch - { - (true, true) => DataChange.ApplyItem | DataChange.Item, - (true, false) => DataChange.ApplyItem, - (false, true) => DataChange.Item, - _ => DataChange.None, - }; + return (currentValue != newValue, currentApply != newApply); } - public static (EquipFlag, CustomizeFlag) ConvertKeysToFlags() + public static (EquipFlag, CustomizeFlag, CrestFlag) ConvertKeysToFlags() => (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch { - (false, false) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant), - (true, true) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant), - (true, false) => (EquipFlagExtensions.All, (CustomizeFlag)0), - (false, true) => ((EquipFlag)0, CustomizeFlagExtensions.AllRelevant), + (false, false) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All), + (true, true) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All), + (true, false) => (EquipFlagExtensions.All, (CustomizeFlag)0, CrestExtensions.All), + (false, true) => ((EquipFlag)0, CustomizeFlagExtensions.AllRelevant, 0), }; public static (bool, bool) ConvertKeysToBool() diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs index a5a46e6..9e9a043 100644 --- a/Glamourer/Interop/ChangeCustomizeService.cs +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -23,7 +23,7 @@ public unsafe class ChangeCustomizeService : EventWrapper _original; /// Check whether we in a manual customize update, in which case we need to not toggle certain flags. - public static readonly ThreadLocal InUpdate = new(() => false); + public static readonly InMethodChecker InUpdate = new(); public enum Priority { @@ -70,9 +70,8 @@ public unsafe class ChangeCustomizeService : EventWrapper(new Customize(*(CustomizeData*)data)); Invoke(this, (Model)human, customize); diff --git a/Glamourer/Interop/CharaFile/CmaFile.cs b/Glamourer/Interop/CharaFile/CmaFile.cs new file mode 100644 index 0000000..15b8af1 --- /dev/null +++ b/Glamourer/Interop/CharaFile/CmaFile.cs @@ -0,0 +1,111 @@ +using System; +using Glamourer.Designs; +using Glamourer.Services; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.CharaFile; + +public sealed class CmaFile +{ + public string Name = string.Empty; + public DesignData Data = new(); + + public static CmaFile? ParseData(ItemManager items, string data, string? name = null) + { + try + { + var jObj = JObject.Parse(data); + var ret = new CmaFile(); + ret.Data.SetDefaultEquipment(items); + ParseMainHand(items, jObj, ref ret.Data); + ParseOffHand(items, jObj, ref ret.Data); + ret.Name = jObj["Description"]?.ToObject() ?? name ?? "New Design"; + ParseEquipment(items, jObj, ref ret.Data); + ParseCustomization(jObj, ref ret.Data); + return ret; + } + catch + { + return null; + } + } + + private static unsafe void ParseCustomization(JObject jObj, ref DesignData data) + { + var bytes = jObj["CharacterBytes"]?.ToObject() ?? string.Empty; + if (bytes.Length is not 26 * 3 - 1) + return; + + bytes = bytes.Replace(" ", string.Empty); + var byteData = Convert.FromHexString(bytes); + fixed (byte* ptr = byteData) + { + data.Customize.Data.Read(ptr); + } + } + + private static unsafe void ParseEquipment(ItemManager items, JObject jObj, ref DesignData data) + { + var bytes = jObj["EquipmentBytes"]?.ToObject() ?? string.Empty; + bytes = bytes.Replace(" ", string.Empty); + var byteData = Convert.FromHexString(bytes); + fixed (byte* ptr = byteData) + { + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var idx = slot.ToIndex(); + if (idx * 4 + 3 >= byteData.Length) + continue; + var armor = ((CharacterArmor*)ptr)[idx]; + var item = items.Identify(slot, armor.Set, armor.Variant); + data.SetItem(slot, item); + data.SetStain(slot, armor.Stain); + } + + data.Customize.Data.Read(ptr); + } + } + + private static void ParseMainHand(ItemManager items, JObject jObj, ref DesignData data) + { + var mainhand = jObj["MainHand"]; + if (mainhand == null) + { + data.SetItem(EquipSlot.MainHand, items.DefaultSword); + data.SetStain(EquipSlot.MainHand, 0); + return; + } + + var set = mainhand["Item1"]?.ToObject() ?? items.DefaultSword.ModelId; + var type = mainhand["Item2"]?.ToObject() ?? items.DefaultSword.WeaponType; + var variant = mainhand["Item3"]?.ToObject() ?? items.DefaultSword.Variant; + var stain = mainhand["Item4"]?.ToObject() ?? 0; + var item = items.Identify(EquipSlot.MainHand, set, type, variant); + + data.SetItem(EquipSlot.MainHand, item.Valid ? item : items.DefaultSword); + data.SetStain(EquipSlot.MainHand, stain); + } + + private static void ParseOffHand(ItemManager items, JObject jObj, ref DesignData data) + { + var offhand = jObj["OffHand"]; + var defaultOffhand = items.GetDefaultOffhand(data.Item(EquipSlot.MainHand)); + if (offhand == null) + { + data.SetItem(EquipSlot.MainHand, defaultOffhand); + data.SetStain(EquipSlot.MainHand, defaultOffhand.ModelId.Id == 0 ? 0 : data.Stain(EquipSlot.MainHand)); + return; + } + + var set = offhand["Item1"]?.ToObject() ?? items.DefaultSword.ModelId; + var type = offhand["Item2"]?.ToObject() ?? items.DefaultSword.WeaponType; + var variant = offhand["Item3"]?.ToObject() ?? items.DefaultSword.Variant; + var stain = offhand["Item4"]?.ToObject() ?? 0; + var item = items.Identify(EquipSlot.OffHand, set, type, variant, data.MainhandType); + + data.SetItem(EquipSlot.OffHand, item.Valid ? item : defaultOffhand); + data.SetStain(EquipSlot.OffHand, defaultOffhand.ModelId.Id == 0 ? 0 : (StainId)stain); + } +} diff --git a/Glamourer/Interop/CrestService.cs b/Glamourer/Interop/CrestService.cs new file mode 100644 index 0000000..9285ec6 --- /dev/null +++ b/Glamourer/Interop/CrestService.cs @@ -0,0 +1,155 @@ +using System; +using System.Linq; +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Interop.Structs; +using Glamourer.Structs; +using OtterGui.Classes; +using Penumbra.GameData.Enums; + +namespace Glamourer.Interop; + +/// +/// Triggered when the crest visibility is updated on a model. +/// +/// Parameter is the model with an update. +/// Parameter is the equipment slot changed. +/// Parameter is the whether the crest will be shown. +/// +/// +public sealed unsafe class CrestService : EventWrapper>, CrestService.Priority> +{ + public enum Priority + { + /// + StateListener = 0, + } + + public CrestService(IGameInteropProvider interop) + : base(nameof(CrestService)) + { + interop.InitializeFromAttributes(this); + _humanSetFreeCompanyCrestVisibleOnSlot = + interop.HookFromAddress(_humanVTable[96], HumanSetFreeCompanyCrestVisibleOnSlotDetour); + _weaponSetFreeCompanyCrestVisibleOnSlot = + interop.HookFromAddress(_weaponVTable[96], WeaponSetFreeCompanyCrestVisibleOnSlotDetour); + _humanSetFreeCompanyCrestVisibleOnSlot.Enable(); + _weaponSetFreeCompanyCrestVisibleOnSlot.Enable(); + _crestChangeHook.Enable(); + } + + public void UpdateCrests(Actor gameObject, CrestFlag flags) + { + if (!gameObject.IsCharacter) + return; + + flags &= CrestExtensions.AllRelevant; + var currentCrests = gameObject.CrestBitfield; + using var update = _inUpdate.EnterMethod(); + _crestChangeHook.Original(gameObject.AsCharacter, (byte) flags); + gameObject.CrestBitfield = currentCrests; + } + + public delegate void DrawObjectCrestUpdateDelegate(Model drawObject, CrestFlag slot, ref bool value); + + public event DrawObjectCrestUpdateDelegate? ModelCrestSetup; + + protected override void Dispose(bool _) + { + _humanSetFreeCompanyCrestVisibleOnSlot.Dispose(); + _weaponSetFreeCompanyCrestVisibleOnSlot.Dispose(); + _crestChangeHook.Dispose(); + } + + private delegate void CrestChangeDelegate(Character* character, byte crestFlags); + + [Signature("E8 ?? ?? ?? ?? 48 8B 55 ?? 49 8B CE E8", DetourName = nameof(CrestChangeDetour))] + private readonly Hook _crestChangeHook = null!; + + private void CrestChangeDetour(Character* character, byte crestFlags) + { + var actor = (Actor)character; + foreach (var slot in CrestExtensions.AllRelevantSet) + { + var newValue = new Ref(((CrestFlag)crestFlags).HasFlag(slot)); + Invoke(this, actor, slot, newValue); + crestFlags = (byte)(newValue.Value ? crestFlags | (byte)slot : crestFlags & (byte)~slot); + } + + Glamourer.Log.Information( + $"Called CrestChange on {(ulong)character:X} with {crestFlags:X} and prior flags {((Actor)character).CrestBitfield}."); + using var _ = _inUpdate.EnterMethod(); + _crestChangeHook.Original(character, crestFlags); + } + + public static bool GetModelCrest(Actor gameObject, CrestFlag slot) + { + if (!gameObject.IsCharacter) + return false; + + var (type, index) = slot.ToIndex(); + switch (type) + { + case CrestType.Human: + { + var model = gameObject.Model; + if (!model.IsHuman) + return false; + + var getter = (delegate* unmanaged)((nint*)model.AsCharacterBase->VTable)[95]; + return getter(model.AsHuman, index) != 0; + } + case CrestType.Offhand: + { + var model = (Model)gameObject.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject; + if (!model.IsWeapon) + return false; + + var getter = (delegate* unmanaged)((nint*)model.AsCharacterBase->VTable)[95]; + return getter(model.AsWeapon, index) != 0; + } + } + + return false; + } + + private readonly InMethodChecker _inUpdate = new(); + + private delegate void SetCrestDelegateIntern(DrawObject* drawObject, byte slot, byte visible); + + [Signature(global::Penumbra.GameData.Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + [Signature(global::Penumbra.GameData.Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _weaponVTable = null!; + + private readonly Hook _humanSetFreeCompanyCrestVisibleOnSlot; + private readonly Hook _weaponSetFreeCompanyCrestVisibleOnSlot; + + private void HumanSetFreeCompanyCrestVisibleOnSlotDetour(DrawObject* drawObject, byte slotIdx, byte visible) + { + var rVisible = visible != 0; + var inUpdate = _inUpdate.InMethod; + var slot = (CrestFlag)((ushort)CrestFlag.Head << slotIdx); + if (!inUpdate) + ModelCrestSetup?.Invoke(drawObject, slot, ref rVisible); + + Glamourer.Log.Excessive( + $"[Human.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{(ulong)drawObject:X} for slot {slot} with {rVisible} (original: {visible != 0}, in update: {inUpdate})."); + _humanSetFreeCompanyCrestVisibleOnSlot.Original(drawObject, slotIdx, rVisible ? (byte)1 : (byte)0); + } + + private void WeaponSetFreeCompanyCrestVisibleOnSlotDetour(DrawObject* drawObject, byte slotIdx, byte visible) + { + var rVisible = visible != 0; + var inUpdate = _inUpdate.InMethod; + if (!inUpdate && slotIdx == 0) + ModelCrestSetup?.Invoke(drawObject, CrestFlag.OffHand, ref rVisible); + Glamourer.Log.Excessive( + $"[Weapon.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{(ulong)drawObject:X} with {rVisible} (original: {visible != 0}, in update: {inUpdate})."); + _weaponSetFreeCompanyCrestVisibleOnSlot.Original(drawObject, slotIdx, rVisible ? (byte)1 : (byte)0); + } +} diff --git a/Glamourer/Interop/ImportService.cs b/Glamourer/Interop/ImportService.cs index 2681abb..217b5fd 100644 --- a/Glamourer/Interop/ImportService.cs +++ b/Glamourer/Interop/ImportService.cs @@ -6,7 +6,9 @@ using Dalamud.Interface.DragDrop; using Dalamud.Interface.Internal.Notifications; using Glamourer.Customization; using Glamourer.Designs; +using Glamourer.Interop.CharaFile; using Glamourer.Services; +using Glamourer.Structs; using ImGuiNET; using OtterGui.Classes; @@ -22,9 +24,9 @@ public class ImportService(CustomizationService _customizations, IDragDropManage }); public void CreateCharaSource() - => _dragDropManager.CreateImGuiSource("CharaDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".chara"), m => + => _dragDropManager.CreateImGuiSource("CharaDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".chara") || m.Extensions.Contains(".cma"), m => { - ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import Anamnesis data for Glamourer..."); + ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import Anamnesis/CMTool data for Glamourer..."); return true; }); @@ -47,8 +49,8 @@ public class ImportService(CustomizationService _customizations, IDragDropManage name = string.Empty; return false; } - - return LoadChara(files[0], out design, out name); + + return Path.GetExtension(files[0]) is ".chara" ? LoadChara(files[0], out design, out name) : LoadCma(files[0], out design, out name); } public bool LoadChara(string path, [NotNullWhen(true)] out DesignBase? design, out string name) @@ -81,6 +83,36 @@ public class ImportService(CustomizationService _customizations, IDragDropManage return true; } + public bool LoadCma(string path, [NotNullWhen(true)] out DesignBase? design, out string name) + { + if (!File.Exists(path)) + { + design = null; + name = string.Empty; + return false; + } + + try + { + var text = File.ReadAllText(path); + var file = CmaFile.ParseData(_items, text, Path.GetFileNameWithoutExtension(path)); + if (file == null) + throw new Exception(); + + name = file.Name; + design = new DesignBase(_customizations, file.Data, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not read .cma file {path}.", NotificationType.Error); + design = null; + name = string.Empty; + return false; + } + + return true; + } + public bool LoadDat(string path, out DatCharacterFile file) { if (!File.Exists(path)) diff --git a/Glamourer/Interop/InventoryService.cs b/Glamourer/Interop/InventoryService.cs index f2832bd..6e72e91 100644 --- a/Glamourer/Interop/InventoryService.cs +++ b/Glamourer/Interop/InventoryService.cs @@ -7,17 +7,20 @@ using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Events; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.String; namespace Glamourer.Interop; public unsafe class InventoryService : IDisposable { - private readonly MovedEquipment _event; + private readonly MovedEquipment _movedItemsEvent; + private readonly EquippedGearset _gearsetEvent; private readonly List<(EquipSlot, uint, StainId)> _itemList = new(12); - public InventoryService(MovedEquipment @event, IGameInteropProvider interop) + public InventoryService(MovedEquipment movedItemsEvent, IGameInteropProvider interop, EquippedGearset gearsetEvent) { - _event = @event; + _movedItemsEvent = movedItemsEvent; + _gearsetEvent = gearsetEvent; _moveItemHook = interop.HookFromAddress((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour); _equipGearsetHook = @@ -39,7 +42,10 @@ public unsafe class InventoryService : IDisposable private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId) { - var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); + var prior = module->CurrentGearsetIndex; + var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); + var set = module->GetGearset(gearsetId); + _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), gearsetId, prior, glamourPlateId, set->ClassJob); Glamourer.Log.Excessive($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); if (ret == 0) { @@ -64,7 +70,7 @@ public unsafe class InventoryService : IDisposable else if (item.GlamourId != 0) _itemList.Add((slot, item.GlamourId, item.Stain)); else - _itemList.Add((slot, item.ItemID, item.Stain)); + _itemList.Add((slot, FixId(item.ItemID), item.Stain)); } var plate = MirageManager.Instance()->GlamourPlatesSpan[glamourPlateId - 1]; @@ -90,7 +96,7 @@ public unsafe class InventoryService : IDisposable else if (item.GlamourId != 0) _itemList.Add((slot, item.GlamourId, item.Stain)); else - _itemList.Add((slot, item.ItemID, item.Stain)); + _itemList.Add((slot, FixId(item.ItemID), item.Stain)); } Add(EquipSlot.MainHand, ref entry->MainHand); @@ -107,12 +113,15 @@ public unsafe class InventoryService : IDisposable Add(EquipSlot.LFinger, ref entry->RingLeft); } - _event.Invoke(_itemList.ToArray()); + _movedItemsEvent.Invoke(_itemList.ToArray()); } return ret; } + private static uint FixId(uint itemId) + => itemId % 50000; + private delegate int MoveItemDelegate(InventoryManager* manager, InventoryType sourceContainer, ushort sourceSlot, InventoryType targetContainer, ushort targetSlot, byte unk); @@ -127,18 +136,18 @@ public unsafe class InventoryService : IDisposable { if (InvokeSource(sourceContainer, sourceSlot, out var source)) if (InvokeTarget(manager, targetContainer, targetSlot, out var target)) - _event.Invoke(new[] + _movedItemsEvent.Invoke(new[] { source, target, }); else - _event.Invoke(new[] + _movedItemsEvent.Invoke(new[] { source, }); else if (InvokeTarget(manager, targetContainer, targetSlot, out var target)) - _event.Invoke(new[] + _movedItemsEvent.Invoke(new[] { target, }); diff --git a/Glamourer/Interop/Structs/Actor.cs b/Glamourer/Interop/Structs/Actor.cs index 1d1d172..750527b 100644 --- a/Glamourer/Interop/Structs/Actor.cs +++ b/Glamourer/Interop/Structs/Actor.cs @@ -3,6 +3,7 @@ using System; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Glamourer.Customization; +using Glamourer.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.String; @@ -106,6 +107,9 @@ public readonly unsafe struct Actor : IEquatable public CharacterArmor GetArmor(EquipSlot slot) => ((CharacterArmor*)&AsCharacter->DrawData.Head)[slot.ToIndex()]; + public bool GetCrest(CrestFlag slot) + => CrestBitfield.HasFlag(slot); + public CharacterWeapon GetMainhand() => new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).ModelId.Value); @@ -115,6 +119,10 @@ public readonly unsafe struct Actor : IEquatable public Customize GetCustomize() => *(Customize*)&AsCharacter->DrawData.CustomizeData; + // TODO remove this when available in ClientStructs + internal ref CrestFlag CrestBitfield + => ref *(CrestFlag*)((byte*)Address + 0x1BBB); + public override string ToString() => $"0x{Address:X}"; } diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index f336f5a..f5a0ec0 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -21,9 +21,7 @@ public unsafe class UpdateSlotService : IDisposable } public void Dispose() - { - _flagSlotForUpdateHook.Dispose(); - } + => _flagSlotForUpdateHook.Dispose(); public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data) { diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index caea959..7d25d82 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -5,6 +5,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using Glamourer.Events; using Glamourer.Interop.Structs; +using Glamourer.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -13,6 +14,7 @@ namespace Glamourer.Interop; public unsafe class WeaponService : IDisposable { private readonly WeaponLoading _event; + private readonly CrestService _crestService; private readonly ThreadLocal _inUpdate = new(() => false); @@ -20,9 +22,10 @@ public unsafe class WeaponService : IDisposable _original; - public WeaponService(WeaponLoading @event, IGameInteropProvider interop) + public WeaponService(WeaponLoading @event, IGameInteropProvider interop, CrestService crestService) { - _event = @event; + _event = @event; + _crestService = crestService; _loadWeaponHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); _original = @@ -64,8 +67,12 @@ public unsafe class WeaponService : IDisposable // First call the regular function. if (equipSlot is not EquipSlot.Unknown) _event.Invoke(actor, equipSlot, ref tmpWeapon); + // Sage hack for weapons appearing in animations? + else if (weaponValue == actor.GetMainhand().Value) + _event.Invoke(actor, EquipSlot.MainHand, ref tmpWeapon); _loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4); + if (tmpWeapon.Value != weapon.Value) { if (tmpWeapon.Set.Id == 0) diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 6435568..253442f 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -165,7 +165,7 @@ public class CommandService : IDisposable .AddInitialPurple("Customizations, ") .AddInitialPurple("Equipment, ") .AddInitialPurple("Accessories, ") - .AddInitialPurple("Dyes and ") + .AddInitialPurple("Dyes & Crests and ") .AddInitialPurple("Weapons, where ").AddPurple("CEADW") .AddText(" means everything should be toggled on, and no value means nothing should be toggled on.") .BuiltString); @@ -268,7 +268,7 @@ public class CommandService : IDisposable applicationFlags |= AutoDesign.Type.Accessories; break; case 'd': - applicationFlags |= AutoDesign.Type.Stains; + applicationFlags |= AutoDesign.Type.GearCustomization; break; case 'w': applicationFlags |= AutoDesign.Type.Weapons; @@ -472,7 +472,7 @@ public class CommandService : IDisposable && _stateManager.GetOrCreate(identifier, data.Objects[0], out state))) continue; - var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant); + var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All); _designManager.CreateClone(design, split[0], true); return true; } diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index 59e8228..745469e 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -66,7 +66,7 @@ public class ItemManager : IDisposable public static EquipItem SmallClothesItem(EquipSlot slot) => new(SmallClothesNpc, SmallclothesId(slot), 0, SmallClothesNpcModel, 0, 1, slot.ToEquipType(), 0, 0, 0); - public EquipItem Resolve(EquipSlot slot, ItemId itemId) + public EquipItem Resolve(EquipSlot slot, CustomItemId itemId) { slot = slot.ToSlot(); if (itemId == NothingId(slot)) @@ -74,7 +74,7 @@ public class ItemManager : IDisposable if (itemId == SmallclothesId(slot)) return SmallClothesItem(slot); - if (!ItemService.AwaitedService.TryGetValue(itemId, slot, out var item)) + if (!itemId.IsItem || !ItemService.AwaitedService.TryGetValue(itemId.Item, slot, out var item)) return EquipItem.FromId(itemId); if (item.Type.ToSlot() != slot) @@ -151,7 +151,7 @@ public class ItemManager : IDisposable /// Returns whether an item id represents a valid item for a slot and gives the item. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public bool IsItemValid(EquipSlot slot, ItemId itemId, out EquipItem item) + public bool IsItemValid(EquipSlot slot, CustomItemId itemId, out EquipItem item) { item = Resolve(slot, itemId); return item.Valid; diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 6e9cffa..93a9854 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -73,6 +73,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); @@ -97,6 +98,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index 2cb3f2a..3cd7cba 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -80,7 +80,8 @@ public class ActorState /// This contains whether a change to the base data was made by the game, the user via manual input or through automatic application. private readonly StateChanged.Source[] _sources = Enumerable - .Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray(); + .Repeat(StateChanged.Source.Game, + EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5 + CrestExtensions.AllRelevantSet.Count).ToArray(); internal ActorState(ActorIdentifier identifier) => Identifier = identifier.CreatePermanent(); @@ -88,6 +89,9 @@ public class ActorState public ref StateChanged.Source this[EquipSlot slot, bool stain] => ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)]; + public ref StateChanged.Source this[CrestFlag slot] + => ref _sources[EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5 + slot.ToInternalIndex()]; + public ref StateChanged.Source this[CustomizeIndex type] => ref _sources[EquipFlagExtensions.NumEquipFlags + (int)type]; diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index a32449e..5eaf672 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -1,13 +1,12 @@ using System.Linq; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Customization; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; +using Glamourer.Structs; using Penumbra.Api.Enums; -using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -17,30 +16,9 @@ namespace Glamourer.State; /// This class applies changes made to state to actual objects in the game. /// It handles applying those changes as well as redrawing the actor if necessary. /// -public class StateApplier +public class StateApplier(UpdateSlotService _updateSlot, VisorService _visor, WeaponService _weapon, ChangeCustomizeService _changeCustomize, + ItemManager _items, PenumbraService _penumbra, MetaService _metaService, ObjectManager _objects, CrestService _crests) { - private readonly PenumbraService _penumbra; - private readonly UpdateSlotService _updateSlot; - private readonly VisorService _visor; - private readonly WeaponService _weapon; - private readonly MetaService _metaService; - private readonly ChangeCustomizeService _changeCustomize; - private readonly ItemManager _items; - private readonly ObjectManager _objects; - - public StateApplier(UpdateSlotService updateSlot, VisorService visor, WeaponService weapon, ChangeCustomizeService changeCustomize, - ItemManager items, PenumbraService penumbra, MetaService metaService, ObjectManager objects) - { - _updateSlot = updateSlot; - _visor = visor; - _weapon = weapon; - _changeCustomize = changeCustomize; - _items = items; - _penumbra = penumbra; - _metaService = metaService; - _objects = objects; - } - /// Simply force a redraw regardless of conditions. public void ForceRedraw(ActorData data) { @@ -279,6 +257,22 @@ public class StateApplier return data; } + /// Change the crest state on actors. + public void ChangeCrests(ActorData data, CrestFlag flags) + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _crests.UpdateCrests(actor, flags); + } + + /// + public ActorData ChangeCrests(ActorState state, bool apply) + { + var data = GetData(state); + if (apply) + ChangeCrests(data, state.ModelData.CrestVisibility); + return data; + } + private ActorData GetData(ActorState state) { _objects.Update(); diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 4bd39b4..3b9d0cb 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -4,6 +4,7 @@ using Dalamud.Plugin.Services; using Glamourer.Customization; using Glamourer.Events; using Glamourer.Services; +using Glamourer.Structs; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -197,6 +198,18 @@ public class StateEditor return true; } + /// Change the crest of an equipment piece. + public bool ChangeCrest(ActorState state, CrestFlag slot, bool crest, StateChanged.Source source, out bool oldCrest, uint key = 0) + { + oldCrest = state.ModelData.Crest(slot); + if (!state.CanUnlock(key)) + return false; + + state.ModelData.SetCrest(slot, crest); + state[slot] = source; + return true; + } + public bool ChangeMetaState(ActorState state, ActorState.MetaIndex index, bool value, StateChanged.Source source, out bool oldValue, uint key = 0) { diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 3fdf4f7..ae6de2f 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -13,6 +13,7 @@ using Penumbra.GameData.Structs; using System; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; +using Glamourer.Structs; namespace Glamourer.State; @@ -42,6 +43,7 @@ public class StateListener : IDisposable private readonly MovedEquipment _movedEquipment; private readonly GPoseService _gPose; private readonly ChangeCustomizeService _changeCustomizeService; + private readonly CrestService _crestService; private readonly ICondition _condition; private ActorIdentifier _creatingIdentifier = ActorIdentifier.Invalid; @@ -52,7 +54,7 @@ public class StateListener : IDisposable SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose, - ChangeCustomizeService changeCustomizeService, CustomizationService customizations, ICondition condition) + ChangeCustomizeService changeCustomizeService, CustomizationService customizations, ICondition condition, CrestService crestService) { _manager = manager; _items = items; @@ -74,6 +76,7 @@ public class StateListener : IDisposable _changeCustomizeService = changeCustomizeService; _customizations = customizations; _condition = condition; + _crestService = crestService; Subscribe(); } @@ -405,6 +408,58 @@ public class StateListener : IDisposable } } + private void OnCrestChange(Actor actor, CrestFlag slot, Ref value) + { + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (!actor.Identifier(_actors.AwaitedService, out var identifier) + || !_manager.TryGetValue(identifier, out var state)) + return; + + switch (UpdateBaseCrest(actor, state, slot, value.Value)) + { + case UpdateState.Change: + if (state[slot] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + _manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), StateChanged.Source.Game); + else + value.Value = state.ModelData.Crest(slot); + break; + case UpdateState.NoChange: + case UpdateState.HatHack: + value.Value = state.ModelData.Crest(slot); + break; + case UpdateState.Transformed: break; + } + } + + private void OnModelCrestSetup(Model model, CrestFlag slot, ref bool value) + { + var actor = _penumbra.GameObjectFromDrawObject(model); + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (!actor.Identifier(_actors.AwaitedService, out var identifier) + || !_manager.TryGetValue(identifier, out var state)) + return; + + value = state.ModelData.Crest(slot); + } + + private static UpdateState UpdateBaseCrest(Actor actor, ActorState state, CrestFlag slot, bool visible) + { + if (actor.IsTransformed) + return UpdateState.Transformed; + + if (state.BaseData.Crest(slot) != visible) + { + state.BaseData.SetCrest(slot, visible); + return UpdateState.Change; + } + + return UpdateState.NoChange; + } + /// Update base data for a single changed weapon slot. private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon) { @@ -490,7 +545,7 @@ public class StateListener : IDisposable private void OnVisorChange(Model model, Ref value) { // Skip updates when in customize update. - if (ChangeCustomizeService.InUpdate.IsValueCreated && ChangeCustomizeService.InUpdate.Value) + if (ChangeCustomizeService.InUpdate.InMethod) return; // Find appropriate actor and state. @@ -616,6 +671,8 @@ public class StateListener : IDisposable _headGearVisibility.Subscribe(OnHeadGearVisibilityChange, HeadGearVisibilityChanged.Priority.StateListener); _weaponVisibility.Subscribe(OnWeaponVisibilityChange, WeaponVisibilityChanged.Priority.StateListener); _changeCustomizeService.Subscribe(OnCustomizeChange, ChangeCustomizeService.Priority.StateListener); + _crestService.Subscribe(OnCrestChange, CrestService.Priority.StateListener); + _crestService.ModelCrestSetup += OnModelCrestSetup; } private void Unsubscribe() @@ -629,6 +686,8 @@ public class StateListener : IDisposable _headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange); _weaponVisibility.Unsubscribe(OnWeaponVisibilityChange); _changeCustomizeService.Unsubscribe(OnCustomizeChange); + _crestService.Unsubscribe(OnCrestChange); + _crestService.ModelCrestSetup -= OnModelCrestSetup; } private void OnCreatedCharacterBase(nint gameObject, string _, nint drawObject) @@ -639,8 +698,9 @@ public class StateListener : IDisposable if (_creatingState == null) return; - _applier.ChangeHatState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsHatVisible()); - _applier.ChangeWeaponState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsWeaponVisible()); - _applier.ChangeWetness(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsWet()); + var data = new ActorData(gameObject, _creatingIdentifier.ToName()); + _applier.ChangeHatState(data, _creatingState.ModelData.IsHatVisible()); + _applier.ChangeWeaponState(data, _creatingState.ModelData.IsWeaponVisible()); + _applier.ChangeWetness(data, _creatingState.ModelData.IsWet()); } } diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 5157c60..68c9cfc 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -10,6 +10,7 @@ using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Structs; using Glamourer.Services; +using Glamourer.Structs; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -17,32 +18,12 @@ using Penumbra.GameData.Structs; namespace Glamourer.State; -public class StateManager : IReadOnlyDictionary +public class StateManager(ActorService _actors, ItemManager _items, StateChanged _event, StateApplier _applier, StateEditor _editor, + HumanModelList _humans, ICondition _condition, IClientState _clientState) + : IReadOnlyDictionary { - private readonly ActorService _actors; - private readonly ItemManager _items; - private readonly HumanModelList _humans; - private readonly StateChanged _event; - private readonly StateApplier _applier; - private readonly StateEditor _editor; - private readonly ICondition _condition; - private readonly IClientState _clientState; - private readonly Dictionary _states = new(); - public StateManager(ActorService actors, ItemManager items, StateChanged @event, StateApplier applier, StateEditor editor, - HumanModelList humans, ICondition condition, IClientState clientState) - { - _actors = actors; - _items = items; - _event = @event; - _applier = applier; - _editor = editor; - _humans = humans; - _condition = condition; - _clientState = clientState; - } - public IEnumerator> GetEnumerator() => _states.GetEnumerator(); @@ -83,7 +64,7 @@ public class StateManager : IReadOnlyDictionary // and the draw objects data for the model data (where possible). state = new ActorState(identifier) { - ModelData = FromActor(actor, true, false), + ModelData = FromActor(actor, true, false), BaseData = FromActor(actor, false, false), LastJob = (byte)(actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0), LastTerritory = _clientState.TerritoryType, @@ -162,6 +143,9 @@ public class StateManager : IReadOnlyDictionary // Visor state is a flag on the game object, but we can see the actual state on the draw object. ret.SetVisor(VisorService.GetVisorState(model)); + + foreach (var slot in CrestExtensions.AllRelevantSet) + ret.SetCrest(slot, CrestService.GetModelCrest(actor, slot)); } else { @@ -180,6 +164,9 @@ public class StateManager : IReadOnlyDictionary off = actor.GetOffhand(); FistWeaponHack(ref ret, ref main, ref off); ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); + + foreach (var slot in CrestExtensions.AllRelevantSet) + ret.SetCrest(slot, actor.GetCrest(slot)); } // Set the weapons regardless of source. @@ -206,7 +193,7 @@ public class StateManager : IReadOnlyDictionary if (mainhand.Set.Id is < 1601 or >= 1651) return; - var gauntlets = _items.Identify(EquipSlot.Hands, offhand.Set, (Variant) offhand.Type.Id); + var gauntlets = _items.Identify(EquipSlot.Hands, offhand.Set, (Variant)offhand.Type.Id); offhand.Set = (SetId)(mainhand.Set.Id + 50); offhand.Variant = mainhand.Variant; offhand.Type = mainhand.Type; @@ -304,6 +291,18 @@ public class StateManager : IReadOnlyDictionary _event.Invoke(StateChanged.Type.Stain, source, state, actors, (old, stain, slot)); } + /// Change the crest of an equipment piece. + public void ChangeCrest(ActorState state, CrestFlag slot, bool crest, StateChanged.Source source, uint key = 0) + { + if (!_editor.ChangeCrest(state, slot, crest, source, out var old, key)) + return; + + var actors = _applier.ChangeCrests(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); + Glamourer.Log.Verbose( + $"Set {slot.ToLabel()} crest in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Crest, source, state, actors, (old, crest, slot)); + } + /// Change hat visibility. public void ChangeHatState(ActorState state, bool value, StateChanged.Source source, uint key = 0) { @@ -356,19 +355,8 @@ public class StateManager : IReadOnlyDictionary public void ApplyDesign(DesignBase design, ActorState state, StateChanged.Source source, uint key = 0) { - void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain) - { - var unused = (applyPiece, applyStain) switch - { - (false, false) => false, - (true, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key), - (false, true) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key), - (true, true) => _editor.ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), source, out _, - out _, key), - }; - } - - if (!_editor.ChangeModelId(state, design.DesignData.ModelId, design.DesignData.Customize, design.GetDesignDataRef().GetEquipmentPtr(), source, + if (!_editor.ChangeModelId(state, design.DesignData.ModelId, design.DesignData.Customize, design.GetDesignDataRef().GetEquipmentPtr(), + source, out var oldModelId, key)) return; @@ -393,12 +381,28 @@ public class StateManager : IReadOnlyDictionary foreach (var slot in EquipSlotExtensions.FullSlots) HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot)); + + foreach (var slot in CrestExtensions.AllRelevantSet.Where(design.DoApplyCrest)) + _editor.ChangeCrest(state, slot, design.DesignData.Crest(slot), source, out _, key); } var actors = ApplyAll(state, redraw, false); Glamourer.Log.Verbose( $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); _event.Invoke(StateChanged.Type.Design, state[ActorState.MetaIndex.Wetness], state, actors, design); + return; + + void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain) + { + var unused = (applyPiece, applyStain) switch + { + (false, false) => false, + (true, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key), + (false, true) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key), + (true, true) => _editor.ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), source, out _, + out _, key), + }; + } } private ActorData ApplyAll(ActorState state, bool redraw, bool withLock) @@ -430,6 +434,7 @@ public class StateManager : IReadOnlyDictionary _applier.ChangeHatState(actors, state.ModelData.IsHatVisible()); _applier.ChangeWeaponState(actors, state.ModelData.IsWeaponVisible()); _applier.ChangeVisor(actors, state.ModelData.IsVisorToggled()); + _applier.ChangeCrests(actors, state.ModelData.CrestVisibility); } return actors; @@ -453,10 +458,13 @@ public class StateManager : IReadOnlyDictionary state[slot, true] = StateChanged.Source.Game; state[slot, false] = StateChanged.Source.Game; } - + foreach (var type in Enum.GetValues()) state[type] = StateChanged.Source.Game; + foreach (var slot in CrestExtensions.AllRelevantSet) + state[slot] = StateChanged.Source.Game; + var actors = ActorData.Invalid; if (source is StateChanged.Source.Manual or StateChanged.Source.Ipc) actors = ApplyAll(state, redraw, true); @@ -491,6 +499,15 @@ public class StateManager : IReadOnlyDictionary } } + foreach (var slot in CrestExtensions.AllRelevantSet) + { + if (state[slot] is StateChanged.Source.Fixed) + { + state[slot] = StateChanged.Source.Game; + state.ModelData.SetCrest(slot, state.BaseData.Crest(slot)); + } + } + if (state[ActorState.MetaIndex.HatState] is StateChanged.Source.Fixed) { state[ActorState.MetaIndex.HatState] = StateChanged.Source.Game; diff --git a/OtterGui b/OtterGui index f55733a..f624aca 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f55733a96fdc9f82c9bbf8272ca6366079aa8e32 +Subproject commit f624aca526bd1f36364d63443ed1c6e83499b8b7