From 63f7818481970fab36c59fc44c8a4e219fa2b430 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 23 Nov 2023 20:44:50 +0100 Subject: [PATCH] FC crest visibility draft --- Glamourer.GameData/Structs/EquipFlag.cs | 70 ++++-- Glamourer/Automation/AutoDesign.cs | 23 +- Glamourer/Automation/AutoDesignApplier.cs | 34 ++- Glamourer/Designs/DesignBase.cs | 40 +++- Glamourer/Designs/DesignData.cs | 23 ++ Glamourer/Designs/DesignManager.cs | 34 +++ Glamourer/Events/CrestVisibilityUpdating.cs | 34 +++ Glamourer/Events/DesignChanged.cs | 6 + Glamourer/Events/StateChanged.cs | 3 + Glamourer/Gui/Equipment/EquipmentDrawer.cs | 217 +++++++++++++++--- Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 42 +++- Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs | 15 +- Glamourer/Gui/Tabs/DebugTab.cs | 24 +- .../DesignTab/DesignFileSystemSelector.cs | 1 + Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 89 ++++--- Glamourer/Gui/UiHelpers.cs | 6 +- Glamourer/Interop/ContextMenuService.cs | 12 +- Glamourer/Interop/Structs/Actor.cs | 19 ++ Glamourer/Interop/Structs/Model.cs | 24 ++ Glamourer/Interop/UpdateSlotService.cs | 95 +++++++- Glamourer/Interop/WeaponService.cs | 38 ++- Glamourer/Services/CommandService.cs | 8 +- Glamourer/Services/ServiceManager.cs | 1 + Glamourer/State/ActorState.cs | 11 +- Glamourer/State/StateApplier.cs | 60 +++-- Glamourer/State/StateEditor.cs | 59 +++-- Glamourer/State/StateListener.cs | 130 ++++++++--- Glamourer/State/StateManager.cs | 85 ++++--- 28 files changed, 980 insertions(+), 223 deletions(-) create mode 100644 Glamourer/Events/CrestVisibilityUpdating.cs diff --git a/Glamourer.GameData/Structs/EquipFlag.cs b/Glamourer.GameData/Structs/EquipFlag.cs index eaacbac..af99b26 100644 --- a/Glamourer.GameData/Structs/EquipFlag.cs +++ b/Glamourer.GameData/Structs/EquipFlag.cs @@ -4,7 +4,7 @@ using Penumbra.GameData.Enums; namespace Glamourer.Structs; [Flags] -public enum EquipFlag : uint +public enum EquipFlag : ulong { Head = 0x00000001, Body = 0x00000002, @@ -30,12 +30,34 @@ public enum EquipFlag : uint LFingerStain = 0x00200000, MainhandStain = 0x00400000, OffhandStain = 0x00800000, + HeadCrest = 0x01000000, + BodyCrest = 0x02000000, + HandsCrest = 0x04000000, + LegsCrest = 0x08000000, + FeetCrest = 0x10000000, + EarsCrest = 0x20000000, + NeckCrest = 0x40000000, + WristCrest = 0x80000000, + RFingerCrest = 0x100000000, + LFingerCrest = 0x200000000, + MainhandCrest = 0x400000000, + OffhandCrest = 0x800000000, } public static class EquipFlagExtensions { - public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1); - public const int NumEquipFlags = 24; + public const EquipFlag All = (EquipFlag)(((ulong)EquipFlag.OffhandCrest << 1) - 1); + public const EquipFlag AllRelevant = All + & ~EquipFlag.HandsCrest + & ~EquipFlag.LegsCrest + & ~EquipFlag.FeetCrest + & ~EquipFlag.EarsCrest + & ~EquipFlag.NeckCrest + & ~EquipFlag.WristCrest + & ~EquipFlag.RFingerCrest + & ~EquipFlag.LFingerCrest + & ~EquipFlag.MainhandCrest; + public const int NumEquipFlags = 36; public static EquipFlag ToFlag(this EquipSlot slot) => slot switch @@ -73,21 +95,39 @@ public static class EquipFlagExtensions _ => 0, }; + public static EquipFlag ToCrestFlag(this EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => EquipFlag.MainhandCrest, + EquipSlot.OffHand => EquipFlag.OffhandCrest, + EquipSlot.Head => EquipFlag.HeadCrest, + EquipSlot.Body => EquipFlag.BodyCrest, + EquipSlot.Hands => EquipFlag.HandsCrest, + EquipSlot.Legs => EquipFlag.LegsCrest, + EquipSlot.Feet => EquipFlag.FeetCrest, + EquipSlot.Ears => EquipFlag.EarsCrest, + EquipSlot.Neck => EquipFlag.NeckCrest, + EquipSlot.Wrists => EquipFlag.WristCrest, + EquipSlot.RFinger => EquipFlag.RFingerCrest, + EquipSlot.LFinger => EquipFlag.LFingerCrest, + _ => 0, + }; + public static EquipFlag ToBothFlags(this EquipSlot slot) => slot switch { - EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain, - EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain, - EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain, - EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain, - EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain, - EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain, - EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain, - EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain, - EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain, - EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain, - EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain, - EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain, + EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain | EquipFlag.MainhandCrest, + EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain | EquipFlag.OffhandCrest, + EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain | EquipFlag.HeadCrest, + EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain | EquipFlag.BodyCrest, + EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain | EquipFlag.HandsCrest, + EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain | EquipFlag.LegsCrest, + EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain | EquipFlag.FeetCrest, + EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain | EquipFlag.EarsCrest, + EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain | EquipFlag.NeckCrest, + EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain | EquipFlag.WristCrest, + EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain | EquipFlag.RFingerCrest, + EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain | EquipFlag.LFingerCrest, _ => 0, }; } diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs index 7beda18..14b01ab 100644 --- a/Glamourer/Automation/AutoDesign.cs +++ b/Glamourer/Automation/AutoDesign.cs @@ -20,8 +20,9 @@ public class AutoDesign Weapons = 0x04, Stains = 0x08, Accessories = 0x10, + Crests = 0x20, - All = Armor | Accessories | Customizations | Weapons | Stains, + All = Armor | Accessories | Customizations | Weapons | Stains | Crests, } public Design? Design; @@ -69,7 +70,8 @@ public class AutoDesign 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.Stains) ? StainFlags : 0) + | (ApplicationType.HasFlag(Type.Crests) ? CrestFlags : 0); var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0; if (Revert) @@ -99,4 +101,21 @@ public class AutoDesign | EquipFlag.WristStain | EquipFlag.RFingerStain | EquipFlag.LFingerStain; + + public const EquipFlag CrestFlags = EquipFlag.MainhandCrest + | EquipFlag.OffhandCrest + | EquipFlag.HeadCrest + | EquipFlag.BodyCrest + | EquipFlag.HandsCrest + | EquipFlag.LegsCrest + | EquipFlag.FeetCrest + | EquipFlag.EarsCrest + | EquipFlag.NeckCrest + | EquipFlag.WristCrest + | EquipFlag.RFingerCrest + | EquipFlag.LFingerCrest; + + public const EquipFlag RelevantCrestFlags = EquipFlag.OffhandCrest + | EquipFlag.HeadCrest + | EquipFlag.BodyCrest; } diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 92cd2b0..4beae0a 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -333,7 +333,7 @@ public class AutoDesignApplier : IDisposable var item = design.Item(slot); if (!_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _)) { - if (!respectManual || state[slot, false] is not StateChanged.Source.Manual) + if (!respectManual || state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Manual) _state.ChangeItem(state, slot, item, source); totalEquipFlags |= flag; } @@ -342,17 +342,25 @@ public class AutoDesignApplier : IDisposable var stainFlag = slot.ToStainFlag(); if (equipFlags.HasFlag(stainFlag)) { - if (!respectManual || state[slot, true] is not StateChanged.Source.Manual) + if (!respectManual || state[slot, ActorState.EquipField.Stain] is not StateChanged.Source.Manual) _state.ChangeStain(state, slot, design.Stain(slot), source); totalEquipFlags |= stainFlag; } + + var crestFlag = slot.ToCrestFlag(); + if (equipFlags.HasFlag(crestFlag)) + { + if (!respectManual || state[slot, ActorState.EquipField.Crest] is not StateChanged.Source.Manual) + _state.ChangeCrest(state, slot, design.Crest(slot), source); + totalEquipFlags |= crestFlag; + } } if (equipFlags.HasFlag(EquipFlag.Mainhand)) { var item = design.Item(EquipSlot.MainHand); var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _); - var checkState = !respectManual || state[EquipSlot.MainHand, false] is not StateChanged.Source.Manual; + var checkState = !respectManual || state[EquipSlot.MainHand, ActorState.EquipField.Item] is not StateChanged.Source.Manual; if (checkUnlock && checkState) { if (fromJobChange) @@ -372,7 +380,7 @@ public class AutoDesignApplier : IDisposable { var item = design.Item(EquipSlot.OffHand); var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _); - var checkState = !respectManual || state[EquipSlot.OffHand, false] is not StateChanged.Source.Manual; + var checkState = !respectManual || state[EquipSlot.OffHand, ActorState.EquipField.Item] is not StateChanged.Source.Manual; if (checkUnlock && checkState) { if (fromJobChange) @@ -390,17 +398,31 @@ public class AutoDesignApplier : IDisposable if (equipFlags.HasFlag(EquipFlag.MainhandStain)) { - if (!respectManual || state[EquipSlot.MainHand, true] is not StateChanged.Source.Manual) + if (!respectManual || state[EquipSlot.MainHand, ActorState.EquipField.Stain] is not StateChanged.Source.Manual) _state.ChangeStain(state, EquipSlot.MainHand, design.Stain(EquipSlot.MainHand), source); totalEquipFlags |= EquipFlag.MainhandStain; } + if (equipFlags.HasFlag(EquipFlag.MainhandCrest)) + { + if (!respectManual || state[EquipSlot.MainHand, ActorState.EquipField.Crest] is not StateChanged.Source.Manual) + _state.ChangeCrest(state, EquipSlot.MainHand, design.Crest(EquipSlot.MainHand), source); + totalEquipFlags |= EquipFlag.MainhandCrest; + } + if (equipFlags.HasFlag(EquipFlag.OffhandStain)) { - if (!respectManual || state[EquipSlot.OffHand, true] is not StateChanged.Source.Manual) + if (!respectManual || state[EquipSlot.OffHand, ActorState.EquipField.Stain] is not StateChanged.Source.Manual) _state.ChangeStain(state, EquipSlot.OffHand, design.Stain(EquipSlot.OffHand), source); totalEquipFlags |= EquipFlag.OffhandStain; } + + if (equipFlags.HasFlag(EquipFlag.OffhandCrest)) + { + if (!respectManual || state[EquipSlot.OffHand, ActorState.EquipField.Crest] is not StateChanged.Source.Manual) + _state.ChangeCrest(state, EquipSlot.OffHand, design.Crest(EquipSlot.OffHand), source); + totalEquipFlags |= EquipFlag.OffhandCrest; + } } private void ReduceCustomize(ActorState state, in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag totalCustomizeFlags, diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index 183ca99..1ab0a10 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -82,7 +82,7 @@ public class DesignBase internal CustomizeFlag ApplyCustomizeRaw => _applyCustomize; - internal EquipFlag ApplyEquip = EquipFlagExtensions.All; + internal EquipFlag ApplyEquip = EquipFlagExtensions.AllRelevant; private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; public bool SetCustomize(CustomizationService customizationService, Customize customize) @@ -166,6 +166,9 @@ public class DesignBase public bool DoApplyStain(EquipSlot slot) => ApplyEquip.HasFlag(slot.ToStainFlag()); + public bool DoApplyCrest(EquipSlot slot) + => ApplyEquip.HasFlag(slot.ToCrestFlag()); + public bool DoApplyCustomize(CustomizeIndex idx) => ApplyCustomize.HasFlag(idx.ToFlag()); @@ -189,6 +192,16 @@ public class DesignBase return true; } + internal bool SetApplyCrest(EquipSlot slot, bool value) + { + var newValue = value ? ApplyEquip | slot.ToCrestFlag() : ApplyEquip & ~slot.ToCrestFlag(); + if (newValue == ApplyEquip) + return false; + + ApplyEquip = newValue; + return true; + } + internal bool SetApplyCustomize(CustomizeIndex idx, bool value) { var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag(); @@ -246,13 +259,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(); @@ -262,7 +277,8 @@ public class DesignBase { var item = _designData.Item(slot); var stain = _designData.Stain(slot); - ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot)); + var crest = _designData.Crest(slot); + ret[slot.ToString()] = Serialize(item.Id, stain, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(slot)); } ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply"); @@ -345,13 +361,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 +380,23 @@ 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)); design._designData.SetItem(slot, item); design._designData.SetStain(slot, stain); + design._designData.SetCrest(slot, crest); design.SetApplyEquip(slot, apply); design.SetApplyStain(slot, applyStain); + design.SetApplyCrest(slot, 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 +407,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(EquipSlot.MainHand, crest); + design._designData.SetCrest(EquipSlot.OffHand, crestOff); design.SetApplyEquip(EquipSlot.MainHand, apply); design.SetApplyEquip(EquipSlot.OffHand, applyOff); design.SetApplyStain(EquipSlot.MainHand, applyStain); design.SetApplyStain(EquipSlot.OffHand, applyStainOff); + design.SetApplyCrest(EquipSlot.MainHand, applyCrest); + design.SetApplyCrest(EquipSlot.OffHand, applyCrestOff); } var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse); design.SetApplyHatVisible(metaValue.Enabled); diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs index 4a24f59..c177e71 100644 --- a/Glamourer/Designs/DesignData.cs +++ b/Glamourer/Designs/DesignData.cs @@ -34,6 +34,7 @@ public unsafe struct DesignData private FullEquipType _typeMainhand; private FullEquipType _typeOffhand; private byte _states; + public ushort CrestVisibility; public bool IsHuman = true; public DesignData() @@ -59,6 +60,15 @@ public unsafe struct DesignData return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3]; } + private static ushort CrestMask(EquipSlot slot) + { + var index = slot.ToIndex(); + return index <= 11 ? (ushort)(1u << (int)index) : (ushort)0; + } + + public readonly bool Crest(EquipSlot slot) + => (CrestVisibility & CrestMask(slot)) != 0; + public FullEquipType MainhandType => _typeMainhand; @@ -173,6 +183,16 @@ public unsafe struct DesignData _ => false, }; + public bool SetCrest(EquipSlot slot, bool visible) + { + var crestVisibility = CrestVisibility; + if (visible) + crestVisibility |= CrestMask(slot); + else + crestVisibility &= (ushort)~CrestMask(slot); + return SetIfDifferent(ref CrestVisibility, crestVisibility); + } + public readonly bool IsWet() => (_states & 0x01) == 0x01; @@ -228,12 +248,15 @@ public unsafe struct DesignData { SetItem(slot, ItemManager.NothingItem(slot)); SetStain(slot, 0); + SetCrest(slot, false); } SetItem(EquipSlot.MainHand, items.DefaultSword); SetStain(EquipSlot.MainHand, 0); + SetCrest(EquipSlot.MainHand, false); SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield)); SetStain(EquipSlot.OffHand, 0); + SetCrest(EquipSlot.OffHand, false); } diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index b8cd9a2..81d3f32 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -446,6 +446,31 @@ public class DesignManager _event.Invoke(DesignChanged.Type.ApplyStain, design, slot); } + /// Change the crest visibility for any equipment piece. + public void ChangeCrest(Design design, EquipSlot 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, EquipSlot 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) { @@ -514,6 +539,9 @@ public class DesignManager if (other.DoApplyStain(slot)) ChangeStain(design, slot, other.DesignData.Stain(slot)); + + if (other.DoApplyCrest(slot)) + ChangeCrest(design, slot, other.DesignData.Crest(slot)); } } @@ -528,6 +556,12 @@ public class DesignManager if (other.DoApplyStain(EquipSlot.OffHand)) ChangeStain(design, EquipSlot.OffHand, other.DesignData.Stain(EquipSlot.OffHand)); + + if (other.DoApplyCrest(EquipSlot.MainHand)) + ChangeCrest(design, EquipSlot.MainHand, other.DesignData.Crest(EquipSlot.MainHand)); + + if (other.DoApplyCrest(EquipSlot.OffHand)) + ChangeCrest(design, EquipSlot.OffHand, other.DesignData.Crest(EquipSlot.OffHand)); } public void UndoDesignChange(Design design) diff --git a/Glamourer/Events/CrestVisibilityUpdating.cs b/Glamourer/Events/CrestVisibilityUpdating.cs new file mode 100644 index 0000000..1c69fe9 --- /dev/null +++ b/Glamourer/Events/CrestVisibilityUpdating.cs @@ -0,0 +1,34 @@ +using System; +using Glamourer.Interop.Structs; +using OtterGui.Classes; +using Penumbra.GameData.Enums; + +namespace Glamourer.Events; + +/// +/// 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 class CrestVisibilityUpdating : EventWrapper>, CrestVisibilityUpdating.Priority> +{ + public enum Priority + { + /// + StateListener = 0, + } + + public CrestVisibilityUpdating() + : base(nameof(CrestVisibilityUpdating)) + { } + + public void Invoke(Model model, EquipSlot slot, ref bool visible) + { + var @return = new Ref(visible); + Invoke(this, model, slot, @return); + visible = @return; + } +} 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/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/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs index 3b33f8a..f209658 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs @@ -13,6 +13,7 @@ using Glamourer.Structs; using Glamourer.Unlocks; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -86,13 +87,13 @@ public class EquipmentDrawer } - 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, + public DataChange DrawEquip(EquipSlot slot, in DesignData designData, out EquipItem rArmor, out StainId rStain, out bool rCrest, EquipFlag? cApply, + out bool rApply, out bool rApplyStain, out bool rApplyCrest, bool locked) + => DrawEquip(slot, designData.Item(slot), out rArmor, designData.Stain(slot), out rStain, designData.Crest(slot), out rCrest, cApply, out rApply, out rApplyStain, out rApplyCrest, 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 DataChange DrawEquip(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, bool cCrest, out bool rCrest, EquipFlag? cApply, + out bool rApply, out bool rApplyStain, out bool rApplyCrest, bool locked, Gender gender = Gender.Unknown, Race race = Race.Unknown) { if (_config.HideApplyCheckmarks) cApply = null; @@ -102,24 +103,26 @@ public class EquipmentDrawer 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); + return DrawEquipSmall(slot, cArmor, out rArmor, cStain, out rStain, cCrest, out rCrest, cApply, out rApply, out rApplyStain, out rApplyCrest, locked, gender, race); if (!locked && _codes.EnabledArtisan) - return DrawEquipArtisan(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain); + return DrawEquipArtisan(slot, cArmor, out rArmor, cStain, out rStain, cCrest, out rCrest, cApply, out rApply, out rApplyStain, out rApplyCrest); - return DrawEquipNormal(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain, locked, gender, race); + return DrawEquipNormal(slot, cArmor, out rArmor, cStain, out rStain, cCrest, out rCrest, cApply, out rApply, out rApplyStain, out rApplyCrest, locked, gender, race); } 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) + out StainId rOffhandStain, out bool rMainhandCrest, out bool rOffhandCrest, EquipFlag? cApply, bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain, + out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest, 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); + designData.Stain(EquipSlot.MainHand), out rMainhandStain, designData.Stain(EquipSlot.OffHand), out rOffhandStain, + designData.Crest(EquipSlot.MainHand), out rMainhandCrest, designData.Crest(EquipSlot.OffHand), out rOffhandCrest, cApply, + allWeapons, out rApplyMainhand, out rApplyMainhandStain, out rApplyMainhandCrest, out rApplyOffhand, out rApplyOffhandStain, out rApplyOffhandCrest, 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, + StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, + bool cMainhandCrest, out bool rMainhandCrest, bool cOffhandCrest, out bool rOffhandCrest, EquipFlag? cApply, + bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest, bool locked) { if (cMainhand.ModelId.Id == 0) @@ -128,10 +131,14 @@ public class EquipmentDrawer rMainhand = cMainhand; rMainhandStain = cMainhandStain; rOffhandStain = cOffhandStain; + rMainhandCrest = cMainhandCrest; + rOffhandCrest = cOffhandCrest; rApplyMainhand = false; rApplyMainhandStain = false; + rApplyMainhandCrest = false; rApplyOffhand = false; rApplyOffhandStain = false; + rApplyOffhandCrest = false; return DataChange.None; } @@ -144,15 +151,18 @@ public class EquipmentDrawer 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, + out rOffhandStain, cMainhandCrest, out rMainhandCrest, cOffhandCrest, out rOffhandCrest, + cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyMainhandCrest, out rApplyOffhand, out rApplyOffhandStain, out rApplyOffhandCrest, 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); + out rOffhandStain, cMainhandCrest, out rMainhandCrest, cOffhandCrest, out rOffhandCrest, + cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyMainhandCrest, out rApplyOffhand, out rApplyOffhandStain, out rApplyOffhandCrest); return DrawWeaponsNormal(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain, - out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked, + out rOffhandStain, cMainhandCrest, out rMainhandCrest, cOffhandCrest, out rOffhandCrest, + cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyMainhandCrest, out rApplyOffhand, out rApplyOffhandStain, out rApplyOffhandCrest, locked, allWeapons); } @@ -331,6 +341,49 @@ public class EquipmentDrawer return change; } + internal static bool CanHaveCrest(EquipSlot slot) + => slot is EquipSlot.OffHand or EquipSlot.Head or EquipSlot.Body; + + private DataChange DrawCrest(EquipSlot slot, bool current, EquipFlag? cApply, out bool ret, out bool rApply, bool locked, bool small, bool change2) + { + var changes = DataChange.None; + if (cApply.HasValue) + { + var apply = cApply.Value.HasFlag(slot.ToCrestFlag()); + var flags = (sbyte)(apply ? current ? 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($"##crest{slot}", flags, out flags)) + { + (ret, rApply) = flags switch + { + -1 => (false, true), + 0 => (current, false), + _ => (true, true), + }; + } + else + { + ret = current; + rApply = apply; + } + } + ImGuiUtil.HoverTooltip($"Display your Free Company crest on this equipment.\n\nThis attribute will be {(apply ? current ? "enabled." : "disabled." : "kept as is.")}"); + if (ret != current) + changes |= change2 ? DataChange.Crest2 : DataChange.Crest; + if (rApply != apply) + changes |= change2 ? DataChange.ApplyCrest2 : DataChange.ApplyCrest; + } + else + { + rApply = false; + if (UiHelpers.DrawCheckbox($"##crest{slot}", "Display your Free Company crest on this equipment.", current, out ret, locked)) + changes |= change2 ? DataChange.Crest2 : DataChange.Crest; + } + return changes; + } + /// Draw an input for armor that can set arbitrary values instead of choosing items. private bool DrawArmorArtisan(EquipSlot slot, EquipItem current, out EquipItem armor) { @@ -383,8 +436,8 @@ public class EquipmentDrawer return false; } - private DataChange DrawEquipArtisan(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, - EquipFlag? cApply, out bool rApply, out bool rApplyStain) + private DataChange DrawEquipArtisan(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, bool cCrest, out bool rCrest, + EquipFlag? cApply, out bool rApply, out bool rApplyStain, out bool rApplyCrest) { var changes = DataChange.None; if (DrawStainArtisan(slot, cStain, out rStain)) @@ -392,6 +445,16 @@ public class EquipmentDrawer ImGui.SameLine(); if (DrawArmorArtisan(slot, cArmor, out rArmor)) changes |= DataChange.Item; + if (CanHaveCrest(slot)) + { + ImGui.SameLine(); + changes |= DrawCrest(slot, cCrest, cApply, out rCrest, out rApplyCrest, false, true, false); + } + else + { + rCrest = cCrest; + rApplyCrest = false; + } if (cApply.HasValue) { ImGui.SameLine(); @@ -410,8 +473,8 @@ public class EquipmentDrawer 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) + private DataChange DrawEquipSmall(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, bool cCrest, out bool rCrest, + EquipFlag? cApply, out bool rApply, out bool rApplyStain, out bool rApplyCrest, bool locked, Gender gender, Race race) { var changes = DataChange.None; if (DrawStain(slot, cStain, out rStain, locked, true)) @@ -419,6 +482,16 @@ public class EquipmentDrawer ImGui.SameLine(); if (DrawItem(slot, cArmor, out rArmor, out var label, locked, true, false, false)) changes |= DataChange.Item; + if (CanHaveCrest(slot)) + { + ImGui.SameLine(); + changes |= DrawCrest(slot, cCrest, cApply, out rCrest, out rApplyCrest, locked, true, false); + } + else + { + rCrest = cCrest; + rApplyCrest = false; + } if (cApply.HasValue) { ImGui.SameLine(); @@ -443,8 +516,8 @@ public class EquipmentDrawer 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 DataChange DrawEquipNormal(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, bool cCrest, out bool rCrest, + EquipFlag? cApply, out bool rApply, out bool rApplyStain, out bool rApplyCrest, bool locked, Gender gender, Race race) { var changes = DataChange.None; cArmor.DrawIcon(_textures, _iconSize, slot); @@ -479,6 +552,16 @@ public class EquipmentDrawer { rApplyStain = true; } + if (CanHaveCrest(slot)) + { + ImGui.SameLine(); + changes |= DrawCrest(slot, cCrest, cApply, out rCrest, out rApplyCrest, locked, false, false); + } + else + { + rCrest = cCrest; + rApplyCrest = true; + } if (VerifyRestrictedGear(slot, rArmor, gender, race)) { @@ -506,8 +589,9 @@ public class EquipmentDrawer } 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, + StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, + bool cMainhandCrest, out bool rMainhandCrest, bool cOffhandCrest, out bool rOffhandCrest, EquipFlag? cApply, + out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest, bool locked, bool allWeapons) { var changes = DataChange.None; @@ -526,6 +610,16 @@ public class EquipmentDrawer } } + if (CanHaveCrest(EquipSlot.MainHand)) + { + ImGui.SameLine(); + changes |= DrawCrest(EquipSlot.MainHand, cMainhandCrest, cApply, out rMainhandCrest, out rApplyMainhandCrest, locked, false, false); + } + else + { + rMainhandCrest = cMainhandCrest; + rApplyMainhandCrest = true; + } if (cApply.HasValue) { ImGui.SameLine(); @@ -548,8 +642,10 @@ public class EquipmentDrawer if (rOffhand.Type is FullEquipType.Unknown) { rOffhandStain = cOffhandStain; + rOffhandCrest = cOffhandCrest; rApplyOffhand = false; rApplyOffhandStain = false; + rApplyOffhandCrest = false; return changes; } @@ -559,6 +655,17 @@ public class EquipmentDrawer ImGui.SameLine(); if (DrawOffhand(rMainhand, rOffhand, out rOffhand, out var offhandLabel, locked, true, false, false)) changes |= DataChange.Item2; + + if (CanHaveCrest(EquipSlot.OffHand)) + { + ImGui.SameLine(); + changes |= DrawCrest(EquipSlot.OffHand, cOffhandCrest, cApply, out rOffhandCrest, out rApplyOffhandCrest, locked, false, true); + } + else + { + rOffhandCrest = cOffhandCrest; + rApplyOffhandCrest = true; + } if (cApply.HasValue) { ImGui.SameLine(); @@ -580,8 +687,9 @@ public class EquipmentDrawer } 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, + StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, + bool cMainhandCrest, out bool rMainhandCrest, bool cOffhandCrest, out bool rOffhandCrest, EquipFlag? cApply, + out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest, bool locked, bool allWeapons) { var changes = DataChange.None; @@ -630,13 +738,26 @@ public class EquipmentDrawer { rApplyMainhandStain = true; } + + if (CanHaveCrest(EquipSlot.MainHand)) + { + ImGui.SameLine(); + changes |= DrawCrest(EquipSlot.MainHand, cMainhandCrest, cApply, out rMainhandCrest, out rApplyMainhandCrest, locked, false, false); + } + else + { + rMainhandCrest = cMainhandCrest; + rApplyMainhandCrest = true; + } } if (rOffhand.Type is FullEquipType.Unknown) { rOffhandStain = cOffhandStain; + rOffhandCrest = cOffhandCrest; rApplyOffhand = false; rApplyOffhandStain = false; + rApplyOffhandCrest = false; return changes; } @@ -673,19 +794,31 @@ public class EquipmentDrawer { rApplyOffhandStain = true; } + + if (CanHaveCrest(EquipSlot.OffHand)) + { + ImGui.SameLine(); + changes |= DrawCrest(EquipSlot.OffHand, cOffhandCrest, cApply, out rOffhandCrest, out rApplyOffhandCrest, locked, false, true); + } + else + { + rOffhandCrest = cOffhandCrest; + rApplyOffhandCrest = 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) + StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, + bool cMainhandCrest, out bool rMainhandCrest, bool cOffhandCrest, out bool rOffhandCrest, EquipFlag? cApply, + out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyMainhandCrest, out bool rApplyOffhand, out bool rApplyOffhandStain, out bool rApplyOffhandCrest) { rApplyMainhand = (cApply ?? 0).HasFlag(EquipFlag.Mainhand); rApplyMainhandStain = (cApply ?? 0).HasFlag(EquipFlag.MainhandStain); rApplyOffhand = (cApply ?? 0).HasFlag(EquipFlag.Offhand); - rApplyOffhandStain = (cApply ?? 0).HasFlag(EquipFlag.MainhandStain); + rApplyOffhandStain = (cApply ?? 0).HasFlag(EquipFlag.OffhandStain); bool DrawWeapon(EquipItem current, out EquipItem ret) { @@ -741,15 +874,35 @@ public class EquipmentDrawer ImGui.SameLine(); if (DrawWeapon(cMainhand, out rMainhand)) ret |= DataChange.Item; + if (CanHaveCrest(EquipSlot.MainHand)) + { + ImGui.SameLine(); + ret |= DrawCrest(EquipSlot.MainHand, cMainhandCrest, cApply, out rMainhandCrest, out rApplyMainhandCrest, false, true, false); + } + else + { + rMainhandCrest = cMainhandCrest; + rApplyMainhandCrest = true; + } } using (var id = ImRaii.PushId(1)) { if (DrawStainArtisan(EquipSlot.OffHand, cOffhandStain, out rOffhandStain)) - ret |= DataChange.Stain; + ret |= DataChange.Stain2; ImGui.SameLine(); if (DrawWeapon(cOffhand, out rOffhand)) - ret |= DataChange.Item; + ret |= DataChange.Item2; + if (CanHaveCrest(EquipSlot.OffHand)) + { + ImGui.SameLine(); + ret |= DrawCrest(EquipSlot.OffHand, cOffhandCrest, cApply, out rOffhandCrest, out rApplyOffhandCrest, false, true, true); + } + else + { + rOffhandCrest = cOffhandCrest; + rApplyOffhandCrest = true; + } } return ret; diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index d049eec..e3e3a7f 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -176,7 +176,7 @@ 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 _, + var changes = _equipmentDrawer.DrawEquip(slot, _state!.ModelData, out var newArmor, out var newStain, out var newCrest, null, out _, out _, out _, _state.IsLocked); if (usedAllStain) { @@ -192,14 +192,20 @@ public class ActorPanel case DataChange.Stain: _stateManager.ChangeStain(_state, slot, newStain, StateChanged.Source.Manual); break; + case DataChange.Crest: + _stateManager.ChangeCrest(_state, slot, newCrest, StateChanged.Source.Manual); + break; case DataChange.Item | DataChange.Stain: - _stateManager.ChangeEquip(_state, slot, newArmor, newStain, StateChanged.Source.Manual); + case DataChange.Item | DataChange.Crest: + case DataChange.Stain | DataChange.Crest: + case DataChange.Item | DataChange.Stain | DataChange.Crest: + _stateManager.ChangeEquip(_state, slot, changes.HasFlag(DataChange.Item) ? newArmor : null, changes.HasFlag(DataChange.Stain) ? newStain : null, changes.HasFlag(DataChange.Crest) ? newCrest : null, StateChanged.Source.Manual); break; } } 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); + out var newOffhandStain, out var newMainhandCrest, out var newOffhandCrest, null, GameMain.IsInGPose(), out _, out _, out _, out _, out _, out _, _state.IsLocked); if (usedAllStain) { weaponChanges |= DataChange.Stain | DataChange.Stain2; @@ -208,20 +214,34 @@ public class ActorPanel } if (weaponChanges.HasFlag(DataChange.Item)) - if (weaponChanges.HasFlag(DataChange.Stain)) - _stateManager.ChangeEquip(_state, EquipSlot.MainHand, newMainhand, newMainhandStain, StateChanged.Source.Manual); + { + if (weaponChanges.HasFlag(DataChange.Stain) || weaponChanges.HasFlag(DataChange.Crest)) + _stateManager.ChangeEquip(_state, EquipSlot.MainHand, newMainhand, weaponChanges.HasFlag(DataChange.Stain) ? newMainhandStain : null, weaponChanges.HasFlag(DataChange.Crest) ? newMainhandCrest : null, 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); + } + else + { + if (weaponChanges.HasFlag(DataChange.Stain)) + _stateManager.ChangeStain(_state, EquipSlot.MainHand, newMainhandStain, StateChanged.Source.Manual); + if (weaponChanges.HasFlag(DataChange.Crest)) + _stateManager.ChangeCrest(_state, EquipSlot.MainHand, newMainhandCrest, StateChanged.Source.Manual); + } if (weaponChanges.HasFlag(DataChange.Item2)) - if (weaponChanges.HasFlag(DataChange.Stain2)) - _stateManager.ChangeEquip(_state, EquipSlot.OffHand, newOffhand, newOffhandStain, StateChanged.Source.Manual); + { + if (weaponChanges.HasFlag(DataChange.Stain2) || weaponChanges.HasFlag(DataChange.Crest2)) + _stateManager.ChangeEquip(_state, EquipSlot.OffHand, newOffhand, weaponChanges.HasFlag(DataChange.Stain2) ? newOffhandStain : null, weaponChanges.HasFlag(DataChange.Crest2) ? newOffhandCrest : null, 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); + } + else + { + if (weaponChanges.HasFlag(DataChange.Stain2)) + _stateManager.ChangeStain(_state, EquipSlot.OffHand, newOffhandStain, StateChanged.Source.Manual); + if (weaponChanges.HasFlag(DataChange.Crest2)) + _stateManager.ChangeCrest(_state, EquipSlot.OffHand, newOffhandCrest, StateChanged.Source.Manual); + } ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); if (EquipmentDrawer.DrawHatState(_state!.ModelData.IsHatVisible(), out var newHatState, _state!.IsLocked)) diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index 3a0f437..01d8baa 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -141,8 +141,8 @@ public class SetPanel { var (numCheckboxes, numSpacing) = (_config.ShowAllAutomatedApplicationRules, _config.ShowUnlockedItemWarnings) switch { - (true, true) => (9, 14), - (true, false) => (7, 10), + (true, true) => (10, 15), + (true, false) => (8, 11), (false, true) => (4, 4), (false, false) => (2, 0), }; @@ -173,7 +173,7 @@ public class SetPanel ImGui.TableSetupColumn("Design", ImGuiTableColumnFlags.WidthFixed, 220 * ImGuiHelpers.GlobalScale); if (_config.ShowAllAutomatedApplicationRules) ImGui.TableSetupColumn("Application", ImGuiTableColumnFlags.WidthFixed, - 6 * ImGui.GetFrameHeight() + 10 * ImGuiHelpers.GlobalScale); + 7 * ImGui.GetFrameHeight() + 11 * ImGuiHelpers.GlobalScale); else ImGui.TableSetupColumn("Use", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); } @@ -182,7 +182,7 @@ public class SetPanel ImGui.TableSetupColumn("Design / Job Restrictions", ImGuiTableColumnFlags.WidthFixed, 250 * ImGuiHelpers.GlobalScale); if (_config.ShowAllAutomatedApplicationRules) ImGui.TableSetupColumn("Application", ImGuiTableColumnFlags.WidthFixed, - 3 * ImGui.GetFrameHeight() + 4 * ImGuiHelpers.GlobalScale); + 4 * ImGui.GetFrameHeight() + 5 * ImGuiHelpers.GlobalScale); else ImGui.TableSetupColumn("Use", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); } @@ -383,14 +383,16 @@ public class SetPanel Box(0); ImGui.SameLine(); Box(1); + ImGui.SameLine(); + Box(2); if (singleLine) ImGui.SameLine(); - Box(2); - ImGui.SameLine(); Box(3); ImGui.SameLine(); Box(4); + ImGui.SameLine(); + Box(5); } _manager.ChangeApplicationType(set, autoDesignIndex, newType); @@ -424,6 +426,7 @@ public class SetPanel (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.Crests, "Apply all crest visibility 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..4f16edd 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -335,14 +335,15 @@ public unsafe class DebugTab : ITab return; if (ImGui.SmallButton("Hide")) - _updateSlotService.UpdateSlot(model, EquipSlot.Head, CharacterArmor.Empty); + _updateSlotService.UpdateSlot(model, EquipSlot.Head, CharacterArmor.Empty, null); ImGui.SameLine(); if (ImGui.SmallButton("Show")) - _updateSlotService.UpdateSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head)); + _updateSlotService.UpdateSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head), actor.GetCrest(EquipSlot.Head)); ImGui.SameLine(); if (ImGui.SmallButton("Toggle")) _updateSlotService.UpdateSlot(model, EquipSlot.Head, - model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty); + model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty, + model.AsHuman->Head.Value == 0 ? actor.GetCrest(EquipSlot.Head) : null); } private void DrawWeaponState(Actor actor, Model model) @@ -423,8 +424,11 @@ public unsafe class DebugTab : ITab if (ImGui.SmallButton("Change Stain")) _updateSlotService.UpdateStain(model, slot, 5); ImGui.SameLine(); + if (ImGui.SmallButton("Toggle Crest")) + _updateSlotService.UpdateCrest(model, slot, !model.IsFreeCompanyCrestVisibleOnSlot((byte)slot.ToIndex())); + ImGui.SameLine(); if (ImGui.SmallButton("Reset")) - _updateSlotService.UpdateSlot(model, slot, actor.GetArmor(slot)); + _updateSlotService.UpdateSlot(model, slot, actor.GetArmor(slot), actor.GetCrest(slot)); } } @@ -1146,7 +1150,7 @@ public unsafe class DebugTab : ITab public void DrawState(ActorData data, ActorState state) { - using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + using var table = ImRaii.Table("##state", 9, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -1189,10 +1193,12 @@ public unsafe class DebugTab : ITab ImGui.TableNextRow(); foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) { - PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state[slot, false]); + PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state[slot, ActorState.EquipField.Item]); ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).Id.ToString()); ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).Id.ToString()); - ImGuiUtil.DrawTableColumn(state[slot, true].ToString()); + ImGuiUtil.DrawTableColumn(state[slot, ActorState.EquipField.Stain].ToString()); + ImGuiUtil.DrawTableColumn(state.BaseData.Crest(slot).ToString()); + ImGuiUtil.DrawTableColumn(state[slot, ActorState.EquipField.Crest].ToString()); } foreach (var type in Enum.GetValues()) @@ -1307,12 +1313,16 @@ public unsafe class DebugTab : ITab var apply = design.DoApplyEquip(slot); var stain = design.DesignData.Stain(slot); var applyStain = design.DoApplyStain(slot); + var crest = design.DesignData.Crest(slot); + var applyCrest = design.DoApplyCrest(slot); ImGuiUtil.DrawTableColumn(slot.ToName()); ImGuiUtil.DrawTableColumn(item.Name); ImGuiUtil.DrawTableColumn(item.ItemId.ToString()); ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep"); ImGuiUtil.DrawTableColumn(stain.ToString()); ImGuiUtil.DrawTableColumn(applyStain ? "Apply" : "Keep"); + ImGuiUtil.DrawTableColumn(crest.ToString()); + ImGuiUtil.DrawTableColumn(applyCrest ? "Apply" : "Keep"); } ImGuiUtil.DrawTableColumn("Hat Visible"); 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 slots) + void ApplyEquip(string label, EquipFlag allFlags, ActorState.EquipField equipField, IEnumerable slots) { - var flags = (uint)(allFlags & _selector.Selected!.ApplyEquip); + // The flags we may edit with a single "big change" checkbox will always fit in a uint, but the whole bitfield doesn't + var shift = (int)equipField; + var flags = (uint)((ulong)(allFlags & _selector.Selected!.ApplyEquip) >> shift); - var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)allFlags); - if (stain) - foreach (var slot in slots) - { - var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToStainFlag()) : _selector.Selected!.DoApplyStain(slot); - if (ImGui.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange) - _manager.ChangeApplyStain(_selector.Selected!, slot, apply); - } - else - foreach (var slot in slots) - { - var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToFlag()) : _selector.Selected!.DoApplyEquip(slot); - if (ImGui.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange) - _manager.ChangeApplyEquip(_selector.Selected!, slot, apply); - } + var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)((ulong)allFlags >> shift)); + + var adjustedFlags = (EquipFlag)((ulong)flags << shift); + switch (equipField) + { + case ActorState.EquipField.Stain: + foreach (var slot in slots) + { + var apply = bigChange ? adjustedFlags.HasFlag(slot.ToStainFlag()) : _selector.Selected!.DoApplyStain(slot); + if (ImGui.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange) + _manager.ChangeApplyStain(_selector.Selected!, slot, apply); + } + break; + case ActorState.EquipField.Crest: + foreach (var slot in slots) + { + var apply = bigChange ? adjustedFlags.HasFlag(slot.ToCrestFlag()) : _selector.Selected!.DoApplyCrest(slot); + if (ImGui.Checkbox($"Apply {slot.ToName()} Crest Visibility", ref apply) || bigChange) + _manager.ChangeApplyCrest(_selector.Selected!, slot, apply); + } + break; + default: + foreach (var slot in slots) + { + var apply = bigChange ? adjustedFlags.HasFlag(slot.ToFlag()) : _selector.Selected!.DoApplyEquip(slot); + if (ImGui.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange) + _manager.ChangeApplyEquip(_selector.Selected!, slot, apply); + } + break; + } } - ApplyEquip("Weapons", AutoDesign.WeaponFlags, false, new[] - { - EquipSlot.MainHand, - EquipSlot.OffHand, - }); + ApplyEquip("Weapons", AutoDesign.WeaponFlags, ActorState.EquipField.Item, EquipSlotExtensions.WeaponSlots); ImGui.NewLine(); - ApplyEquip("Armor", AutoDesign.ArmorFlags, false, EquipSlotExtensions.EquipmentSlots); + ApplyEquip("Armor", AutoDesign.ArmorFlags, ActorState.EquipField.Item, EquipSlotExtensions.EquipmentSlots); ImGui.NewLine(); - ApplyEquip("Accessories", AutoDesign.AccessoryFlags, false, EquipSlotExtensions.AccessorySlots); + ApplyEquip("Accessories", AutoDesign.AccessoryFlags, ActorState.EquipField.Item, EquipSlotExtensions.AccessorySlots); ImGui.NewLine(); - ApplyEquip("Dyes", AutoDesign.StainFlags, true, + ApplyEquip("Dyes", AutoDesign.StainFlags, ActorState.EquipField.Stain, EquipSlotExtensions.FullSlots); + ImGui.NewLine(); + ApplyEquip("Crest Visibilities", AutoDesign.RelevantCrestFlags, ActorState.EquipField.Crest, CrestSlots); + ImGui.NewLine(); const uint all = 0x0Fu; var flags = (_selector.Selected!.DoApplyHatVisible() ? 0x01u : 0x00) diff --git a/Glamourer/Gui/UiHelpers.cs b/Glamourer/Gui/UiHelpers.cs index eb1f1c2..28c1172 100644 --- a/Glamourer/Gui/UiHelpers.cs +++ b/Glamourer/Gui/UiHelpers.cs @@ -17,7 +17,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Gui; [Flags] -public enum DataChange : byte +public enum DataChange : ushort { None = 0x00, Item = 0x01, @@ -28,6 +28,10 @@ public enum DataChange : byte Stain2 = 0x20, ApplyItem2 = 0x40, ApplyStain2 = 0x80, + Crest = 0x100, + ApplyCrest = 0x200, + Crest2 = 0x400, + ApplyCrest2 = 0x800, } public static class UiHelpers diff --git a/Glamourer/Interop/ContextMenuService.cs b/Glamourer/Interop/ContextMenuService.cs index c210f1f..e0cc4b7 100644 --- a/Glamourer/Interop/ContextMenuService.cs +++ b/Glamourer/Interop/ContextMenuService.cs @@ -119,14 +119,14 @@ public class ContextMenuService : IDisposable return; var slot = item.Type.ToSlot(); - _state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual); + _state.ChangeEquip(state, slot, item, 0, false, StateChanged.Source.Manual); if (item.Type.ValidOffhand().IsOffhandType()) { if (item.ModelId.Id is > 1600 and < 1651 && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets)) - _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual); + _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, false, StateChanged.Source.Manual); if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) - _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual); + _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, false, StateChanged.Source.Manual); } }; } @@ -143,14 +143,14 @@ public class ContextMenuService : IDisposable return; var slot = item.Type.ToSlot(); - _state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual); + _state.ChangeEquip(state, slot, item, 0, false, StateChanged.Source.Manual); if (item.Type.ValidOffhand().IsOffhandType()) { if (item.ModelId.Id is > 1600 and < 1651 && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets)) - _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual); + _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, false, StateChanged.Source.Manual); if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) - _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual); + _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, false, StateChanged.Source.Manual); } }; } diff --git a/Glamourer/Interop/Structs/Actor.cs b/Glamourer/Interop/Structs/Actor.cs index 1d1d172..03a7c03 100644 --- a/Glamourer/Interop/Structs/Actor.cs +++ b/Glamourer/Interop/Structs/Actor.cs @@ -106,6 +106,21 @@ public readonly unsafe struct Actor : IEquatable public CharacterArmor GetArmor(EquipSlot slot) => ((CharacterArmor*)&AsCharacter->DrawData.Head)[slot.ToIndex()]; + public bool GetCrest(EquipSlot slot) + => (GetFreeCompanyCrestBitfield() & CrestMask(slot)) != 0; + + private static byte CrestMask(EquipSlot slot) + => slot switch + { + EquipSlot.OffHand => 0x01, + EquipSlot.Head => 0x02, + EquipSlot.Body => 0x04, + EquipSlot.Hands => 0x08, + EquipSlot.Legs => 0x10, + EquipSlot.Feet => 0x20, + _ => 0x00, + }; + public CharacterWeapon GetMainhand() => new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).ModelId.Value); @@ -115,6 +130,10 @@ public readonly unsafe struct Actor : IEquatable public Customize GetCustomize() => *(Customize*)&AsCharacter->DrawData.CustomizeData; + // TODO remove this when available in ClientStructs + public byte GetFreeCompanyCrestBitfield() + => ((byte*)Address)[0x1BBB]; + public override string ToString() => $"0x{Address:X}"; } diff --git a/Glamourer/Interop/Structs/Model.cs b/Glamourer/Interop/Structs/Model.cs index 77bf24e..b43ea7e 100644 --- a/Glamourer/Interop/Structs/Model.cs +++ b/Glamourer/Interop/Structs/Model.cs @@ -91,6 +91,9 @@ public readonly unsafe struct Model : IEquatable public CharacterArmor GetArmor(EquipSlot slot) => ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()]; + public bool GetCrest(EquipSlot slot) + => IsFreeCompanyCrestVisibleOnSlot((byte)slot.ToIndex()); + public Customize GetCustomize() => *(Customize*)&AsHuman->Customize; @@ -195,6 +198,27 @@ public readonly unsafe struct Model : IEquatable return discriminator1 == 0 && discriminator2 != 0 ? (second, first) : (first, second); } + // TODO remove these when available in ClientStructs + public bool IsFreeCompanyCrestVisibleOnSlot(byte slot) + { + if (!IsCharacterBase) + return false; + + var characterBase = AsCharacterBase; + var getter = (delegate* unmanaged)((nint*)characterBase->VTable)[95]; + return getter(characterBase, slot) != 0; + } + + public void SetFreeCompanyCrestVisibleOnSlot(byte slot, bool visible) + { + if (!IsCharacterBase) + return; + + var characterBase = AsCharacterBase; + var setter = (delegate* unmanaged)((nint*)characterBase->VTable)[96]; + setter(characterBase, slot, visible ? (byte)1 : (byte)0); + } + public override string ToString() => $"0x{Address:X}"; } diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index f336f5a..062af5e 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -1,4 +1,6 @@ using System; +using System.Runtime.CompilerServices; +using System.Threading; using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; @@ -6,47 +8,78 @@ using Glamourer.Events; using Glamourer.Interop.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using PenumbraSigs = Penumbra.GameData.Sigs; namespace Glamourer.Interop; public unsafe class UpdateSlotService : IDisposable { - public readonly SlotUpdating SlotUpdatingEvent; + public readonly SlotUpdating SlotUpdatingEvent; + public readonly CrestVisibilityUpdating CrestVisibilityUpdatingEvent; + private readonly ThreadLocal _crestVisibilityUpdate = new(() => 0, false); - public UpdateSlotService(SlotUpdating slotUpdating, IGameInteropProvider interop) + public UpdateSlotService(SlotUpdating slotUpdating, CrestVisibilityUpdating crestVisibilityUpdating, IGameInteropProvider interop) { - SlotUpdatingEvent = slotUpdating; + SlotUpdatingEvent = slotUpdating; + CrestVisibilityUpdatingEvent = crestVisibilityUpdating; interop.InitializeFromAttributes(this); + _humanSetFreeCompanyCrestVisibleOnSlot = interop.HookFromAddress(_humanVTable[96], HumanSetFreeCompanyCrestVisibleOnSlotDetour); + _weaponSetFreeCompanyCrestVisibleOnSlot = interop.HookFromAddress(_weaponVTable[96], WeaponSetFreeCompanyCrestVisibleOnSlotDetour); _flagSlotForUpdateHook.Enable(); + _humanSetFreeCompanyCrestVisibleOnSlot.Enable(); + _weaponSetFreeCompanyCrestVisibleOnSlot.Enable(); } public void Dispose() { _flagSlotForUpdateHook.Dispose(); + _humanSetFreeCompanyCrestVisibleOnSlot.Dispose(); + _weaponSetFreeCompanyCrestVisibleOnSlot.Dispose(); } - public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data) + public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data, bool? crest) { if (!drawObject.IsCharacterBase) return; FlagSlotForUpdateInterop(drawObject, slot, data); + if (crest.HasValue) + { + using var _ = EnterCrestVisibilityUpdate(); + drawObject.SetFreeCompanyCrestVisibleOnSlot((byte)slot.ToIndex(), crest.Value); + } } - public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain) - => UpdateSlot(drawObject, slot, armor.With(stain)); + public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain, bool? crest) + => UpdateSlot(drawObject, slot, armor.With(stain), crest); public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor) - => UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain); + => UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain, null); public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain) - => UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain); + => UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain, null); + + public void UpdateCrest(Model drawObject, EquipSlot slot, bool crest) + { + using var _ = EnterCrestVisibilityUpdate(); + drawObject.SetFreeCompanyCrestVisibleOnSlot((byte)slot.ToIndex(), crest); + } private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data); + private delegate void SetCrestDelegateIntern(nint drawObject, byte slot, byte visible); [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))] private readonly Hook _flagSlotForUpdateHook = null!; + [Signature(PenumbraSigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + [Signature(PenumbraSigs.WeaponVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _weaponVTable = null!; + + private readonly Hook _humanSetFreeCompanyCrestVisibleOnSlot; + private readonly Hook _weaponSetFreeCompanyCrestVisibleOnSlot; + private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { var slot = slotIdx.ToEquipSlot(); @@ -56,6 +89,52 @@ public unsafe class UpdateSlotService : IDisposable return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; } + private void HumanSetFreeCompanyCrestVisibleOnSlotDetour(nint drawObject, byte slotIdx, byte visible) + { + var slot = ((uint)slotIdx).ToEquipSlot(); + var rVisible = visible != 0; + var inUpdate = _crestVisibilityUpdate.IsValueCreated && _crestVisibilityUpdate.Value > 0; + if (!inUpdate) + CrestVisibilityUpdatingEvent.Invoke(drawObject, slot, ref rVisible); + Glamourer.Log.Excessive($"[Human.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{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(nint drawObject, byte slotIdx, byte visible) + { + var rVisible = visible != 0; + var inUpdate = _crestVisibilityUpdate.IsValueCreated && _crestVisibilityUpdate.Value > 0; + if (!inUpdate) + CrestVisibilityUpdatingEvent.Invoke(drawObject, EquipSlot.BothHand, ref rVisible); + Glamourer.Log.Excessive($"[Weapon.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{drawObject:X} with {rVisible} (original: {visible != 0}, in update: {inUpdate})."); + _weaponSetFreeCompanyCrestVisibleOnSlot.Original(drawObject, slotIdx, rVisible ? (byte)1 : (byte)0); + } + private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); + + /// + /// Temporarily disables the crest update hooks on the current thread. + /// + /// A struct that will undo this operation when disposed. Best used with: using (var _ = updateSlotService.EnterCrestVisibilityUpdate()) { ... } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public CrestVisibilityUpdateRaii EnterCrestVisibilityUpdate() + => new(this); + + public readonly ref struct CrestVisibilityUpdateRaii + { + private readonly ThreadLocal _crestVisibilityUpdate; + + public CrestVisibilityUpdateRaii(UpdateSlotService parent) + { + _crestVisibilityUpdate = parent._crestVisibilityUpdate; + ++_crestVisibilityUpdate.Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public readonly void Dispose() + { + --_crestVisibilityUpdate.Value; + } + } } diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index caea959..8caa85c 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -13,6 +13,7 @@ namespace Glamourer.Interop; public unsafe class WeaponService : IDisposable { private readonly WeaponLoading _event; + private readonly UpdateSlotService _updateSlotService; private readonly ThreadLocal _inUpdate = new(() => false); @@ -20,9 +21,10 @@ public unsafe class WeaponService : IDisposable _original; - public WeaponService(WeaponLoading @event, IGameInteropProvider interop) + public WeaponService(WeaponLoading @event, UpdateSlotService updateSlotService, IGameInteropProvider interop) { _event = @event; + _updateSlotService = updateSlotService; _loadWeaponHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); _original = @@ -83,24 +85,39 @@ public unsafe class WeaponService : IDisposable } // Load a specific weapon for a character by its data and slot. - public void LoadWeapon(Actor character, EquipSlot slot, CharacterWeapon weapon) + public void LoadWeapon(Actor character, EquipSlot slot, CharacterWeapon weapon, bool? crest) { switch (slot) { case EquipSlot.MainHand: _inUpdate.Value = true; _loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0); + if (crest.HasValue) + { + using var _ = _updateSlotService.EnterCrestVisibilityUpdate(); + character.Model.GetMainhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, crest.Value); + } _inUpdate.Value = false; return; case EquipSlot.OffHand: _inUpdate.Value = true; _loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0); + if (crest.HasValue) + { + using var _ = _updateSlotService.EnterCrestVisibilityUpdate(); + character.Model.GetOffhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, crest.Value); + } _inUpdate.Value = false; return; case EquipSlot.BothHand: _inUpdate.Value = true; _loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0); _loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0); + if (crest.HasValue) + { + using var _ = _updateSlotService.EnterCrestVisibilityUpdate(); + character.Model.GetMainhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, crest.Value); + } _inUpdate.Value = false; return; } @@ -112,6 +129,21 @@ public unsafe class WeaponService : IDisposable var (_, _, mh, oh) = mdl.GetWeapons(character); var value = slot == EquipSlot.OffHand ? oh : mh; var weapon = value.With(value.Set.Id == 0 ? 0 : stain); - LoadWeapon(character, slot, weapon); + LoadWeapon(character, slot, weapon, null); + } + + public void UpdateCrest(Actor character, EquipSlot slot, bool visible) + { + using var _ = _updateSlotService.EnterCrestVisibilityUpdate(); + switch (slot) + { + case EquipSlot.MainHand: + case EquipSlot.BothHand: + character.Model.GetMainhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, visible); + break; + case EquipSlot.OffHand: + character.Model.GetOffhand().Address.SetFreeCompanyCrestVisibleOnSlot(0, visible); + break; + } } } diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 6435568..fbbad07 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -165,8 +165,9 @@ public class CommandService : IDisposable .AddInitialPurple("Customizations, ") .AddInitialPurple("Equipment, ") .AddInitialPurple("Accessories, ") - .AddInitialPurple("Dyes and ") - .AddInitialPurple("Weapons, where ").AddPurple("CEADW") + .AddInitialPurple("Dyes, ") + .AddInitialPurple("Free company crest visibilities and ") + .AddInitialPurple("Weapons, where ").AddPurple("CEADFW") .AddText(" means everything should be toggled on, and no value means nothing should be toggled on.") .BuiltString); return false; @@ -270,6 +271,9 @@ public class CommandService : IDisposable case 'd': applicationFlags |= AutoDesign.Type.Stains; break; + case 'f': + applicationFlags |= AutoDesign.Type.Crests; + break; case 'w': applicationFlags |= AutoDesign.Type.Weapons; break; diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 6e9cffa..c5ba47d 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -64,6 +64,7 @@ public static class ServiceManager private static IServiceCollection AddEvents(this IServiceCollection services) => services.AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index 2cb3f2a..e943e8d 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -22,6 +22,13 @@ public class ActorState ModelId, } + public enum EquipField + { + Item = 0, + Stain = 12, + Crest = 24, + } + public readonly ActorIdentifier Identifier; public bool AllowsRedraw(ICondition condition) @@ -85,8 +92,8 @@ public class ActorState internal ActorState(ActorIdentifier identifier) => Identifier = identifier.CreatePermanent(); - public ref StateChanged.Source this[EquipSlot slot, bool stain] - => ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)]; + public ref StateChanged.Source this[EquipSlot slot, EquipField field] + => ref _sources[slot.ToIndex() + (int)field]; 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..91d98c4 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -99,11 +99,11 @@ public class StateApplier } /// - /// Change a single piece of armor and/or stain depending on slot. + /// Change a single piece of armor and/or stain and/or crest visibility depending on slot. /// This uses the current customization of the model to potentially prevent restricted gear types from appearing. /// This never requires redrawing. /// - public void ChangeArmor(ActorData data, EquipSlot slot, CharacterArmor armor, bool checkRestrictions, bool isHatVisible = true) + public void ChangeArmor(ActorData data, EquipSlot slot, CharacterArmor armor, bool crest, bool checkRestrictions, bool isHatVisible = true) { if (slot is EquipSlot.Head && !isHatVisible) return; @@ -118,11 +118,11 @@ public class StateApplier { var customize = mdl.GetCustomize(); var (_, resolvedItem) = _items.ResolveRestrictedGear(armor, slot, customize.Race, customize.Gender); - _updateSlot.UpdateSlot(actor.Model, slot, resolvedItem); + _updateSlot.UpdateSlot(actor.Model, slot, resolvedItem, crest); } else { - _updateSlot.UpdateSlot(actor.Model, slot, armor); + _updateSlot.UpdateSlot(actor.Model, slot, armor, crest); } } } @@ -133,7 +133,7 @@ public class StateApplier // If the source is not IPC we do not want to apply restrictions. var data = GetData(state); if (apply) - ChangeArmor(data, slot, state.ModelData.Armor(slot), state[slot, false] is not StateChanged.Source.Ipc, + ChangeArmor(data, slot, state.ModelData.Armor(slot), state.ModelData.Crest(slot), state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Ipc, state.ModelData.IsHatVisible()); return data; @@ -175,13 +175,47 @@ public class StateApplier } + /// + /// Change the crest visibility of a single piece of armor or weapon. + /// + public void ChangeCrest(ActorData data, EquipSlot slot, bool visible) + { + var idx = slot.ToIndex(); + switch (idx) + { + case < 10: + foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) + _updateSlot.UpdateCrest(actor.Model, slot, visible); + break; + case 10: + foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) + _weapon.UpdateCrest(actor, EquipSlot.MainHand, visible); + break; + case 11: + foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) + _weapon.UpdateCrest(actor, EquipSlot.OffHand, visible); + break; + } + } + + /// + public ActorData ChangeCrest(ActorState state, EquipSlot slot, bool apply) + { + var data = GetData(state); + if (apply) + ChangeCrest(data, slot, state.ModelData.Crest(slot)); + + return data; + } + + /// Apply a weapon to the appropriate slot. - public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainId stain) + public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainId stain, bool crest) { if (slot is EquipSlot.MainHand) - ChangeMainhand(data, item, stain); + ChangeMainhand(data, item, stain, crest); else - ChangeOffhand(data, item, stain); + ChangeOffhand(data, item, stain, crest); } /// @@ -192,7 +226,7 @@ public class StateApplier data = data.OnlyGPose(); if (apply) - ChangeWeapon(data, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot)); + ChangeWeapon(data, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot), state.ModelData.Crest(slot)); return data; } @@ -200,19 +234,19 @@ public class StateApplier /// /// Apply a weapon to the mainhand. If the weapon type has no associated offhand type, apply both. /// - public void ChangeMainhand(ActorData data, EquipItem weapon, StainId stain) + public void ChangeMainhand(ActorData data, EquipItem weapon, StainId stain, bool crest) { var slot = weapon.Type.ValidOffhand() == FullEquipType.Unknown ? EquipSlot.BothHand : EquipSlot.MainHand; foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stain)); + _weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stain), crest); } /// Apply a weapon to the offhand. - public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain) + public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain, bool crest) { stain = weapon.ModelId.Id == 0 ? 0 : stain; foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain)); + _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain), crest); } /// Change the visor state of actors only on the draw object. diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 4bd39b4..26d6005 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -67,8 +67,9 @@ public class StateEditor state[ActorState.MetaIndex.VisorState] = source; foreach (var slot in EquipSlotExtensions.FullSlots) { - state[slot, true] = source; - state[slot, false] = source; + state[slot, ActorState.EquipField.Stain] = source; + state[slot, ActorState.EquipField.Item] = source; + state[slot, ActorState.EquipField.Crest] = source; } state[CustomizeIndex.Clan] = source; @@ -126,7 +127,7 @@ public class StateEditor return true; } - /// Change a single piece of equipment without stain. + /// Change a single piece of equipment without stain or crest visibility. public bool ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source, out EquipItem oldItem, uint key = 0) { oldItem = state.ModelData.Item(slot); @@ -144,44 +145,58 @@ public class StateEditor _gPose.AddActionOnLeave(() => { if (old.Type == state.BaseData.Item(slot).Type) - ChangeItem(state, slot, old, state[slot, false], out _, key); + ChangeItem(state, slot, old, state[slot, ActorState.EquipField.Item], out _, key); }); } state.ModelData.SetItem(slot, item); - state[slot, false] = source; + state[slot, ActorState.EquipField.Item] = source; return true; } - /// Change a single piece of equipment including stain. - public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source, out EquipItem oldItem, - out StainId oldStain, uint key = 0) + /// Change a single piece of equipment including stain and crest visibility. + public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem? item, StainId? stain, bool? crest, StateChanged.Source source, out EquipItem oldItem, + out StainId oldStain, out bool oldCrest, uint key = 0) { oldItem = state.ModelData.Item(slot); oldStain = state.ModelData.Stain(slot); + oldCrest = state.ModelData.Crest(slot); + if (!state.CanUnlock(key)) return false; // Can not change weapon type from expected type in state. - if (slot is EquipSlot.MainHand && item.Type != state.BaseData.MainhandType - || slot is EquipSlot.OffHand && item.Type != state.BaseData.OffhandType) + if (item.HasValue && (slot is EquipSlot.MainHand && item.Value.Type != state.BaseData.MainhandType + || slot is EquipSlot.OffHand && item.Value.Type != state.BaseData.OffhandType)) { if (!_gPose.InGPose) return false; var old = oldItem; var oldS = oldStain; + var oldC = oldCrest; _gPose.AddActionOnLeave(() => { if (old.Type == state.BaseData.Item(slot).Type) - ChangeEquip(state, slot, old, oldS, state[slot, false], out _, out _, key); + ChangeEquip(state, slot, old, oldS, oldC, state[slot, ActorState.EquipField.Item], out _, out _, out _, key); }); } - state.ModelData.SetItem(slot, item); - state.ModelData.SetStain(slot, stain); - state[slot, false] = source; - state[slot, true] = source; + if (item.HasValue) + { + state.ModelData.SetItem(slot, item.Value); + state[slot, ActorState.EquipField.Item] = source; + } + if (stain.HasValue) + { + state.ModelData.SetStain(slot, stain.Value); + state[slot, ActorState.EquipField.Stain] = source; + } + if (crest.HasValue) + { + state.ModelData.SetCrest(slot, crest.Value); + state[slot, ActorState.EquipField.Crest] = source; + } return true; } @@ -193,7 +208,19 @@ public class StateEditor return false; state.ModelData.SetStain(slot, stain); - state[slot, true] = source; + state[slot, ActorState.EquipField.Stain] = source; + return true; + } + + /// Change only the crest visibility of an equipment piece. + public bool ChangeCrest(ActorState state, EquipSlot 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, ActorState.EquipField.Crest] = source; return true; } diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 3fdf4f7..bec01b4 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -32,6 +32,7 @@ public class StateListener : IDisposable private readonly CustomizationService _customizations; private readonly PenumbraService _penumbra; private readonly SlotUpdating _slotUpdating; + private readonly CrestVisibilityUpdating _crestVisibilityUpdating; private readonly WeaponLoading _weaponLoading; private readonly HeadGearVisibilityChanged _headGearVisibility; private readonly VisorStateChanged _visorState; @@ -51,29 +52,30 @@ public class StateListener : IDisposable public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config, SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans, - StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose, + CrestVisibilityUpdating crestVisibilityUpdating, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizationService customizations, ICondition condition) { - _manager = manager; - _items = items; - _penumbra = penumbra; - _actors = actors; - _config = config; - _slotUpdating = slotUpdating; - _weaponLoading = weaponLoading; - _visorState = visorState; - _weaponVisibility = weaponVisibility; - _headGearVisibility = headGearVisibility; - _autoDesignApplier = autoDesignApplier; - _funModule = funModule; - _humans = humans; - _applier = applier; - _movedEquipment = movedEquipment; - _objects = objects; - _gPose = gPose; - _changeCustomizeService = changeCustomizeService; - _customizations = customizations; - _condition = condition; + _manager = manager; + _items = items; + _penumbra = penumbra; + _actors = actors; + _config = config; + _slotUpdating = slotUpdating; + _crestVisibilityUpdating = crestVisibilityUpdating; + _weaponLoading = weaponLoading; + _visorState = visorState; + _weaponVisibility = weaponVisibility; + _headGearVisibility = headGearVisibility; + _autoDesignApplier = autoDesignApplier; + _funModule = funModule; + _humans = humans; + _applier = applier; + _movedEquipment = movedEquipment; + _objects = objects; + _gPose = gPose; + _changeCustomizeService = changeCustomizeService; + _customizations = customizations; + _condition = condition; Subscribe(); } @@ -212,7 +214,7 @@ public class StateListener : IDisposable && _manager.TryGetValue(identifier, out var state)) { HandleEquipSlot(actor, state, slot, ref armor.Value); - locked = state[slot, false] is StateChanged.Source.Ipc; + locked = state[slot, ActorState.EquipField.Item] is StateChanged.Source.Ipc; } _funModule.ApplyFun(actor, ref armor.Value, slot); @@ -223,6 +225,34 @@ public class StateListener : IDisposable (_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); } + private void OnCrestVisibilityUpdating(Model model, EquipSlot slot, Ref visible) + { + var actor = _penumbra.GameObjectFromDrawObject(model); + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (slot == EquipSlot.BothHand) + { + var rootModel = actor.Model; + var (mainHand, offHand, _, _) = rootModel.GetWeapons(actor); + if (model == mainHand) + slot = EquipSlot.MainHand; + else if (model == offHand) + slot = EquipSlot.OffHand; + else + { + Glamourer.Log.Excessive($"Cannot identify weapon slot: got model {model.Address:X}, main hand is {mainHand.Address:X}, off hand is {offHand.Address:X}"); + return; + } + } + + if (actor.Identifier(_actors.AwaitedService, out var identifier) + && _manager.TryGetValue(identifier, out var state)) + { + HandleCrestVisibility(actor, state, slot, ref visible.Value); + } + } + private void OnMovedEquipment((EquipSlot, uint, StainId)[] items) { _objects.Update(); @@ -235,11 +265,12 @@ public class StateListener : IDisposable var currentItem = state.BaseData.Item(slot); var model = state.ModelData.Weapon(slot); var current = currentItem.Weapon(state.BaseData.Stain(slot)); + var crest = state.BaseData.Crest(slot); if (model.Value == current.Value || !_items.ItemService.AwaitedService.TryGetValue(item, EquipSlot.MainHand, out var changedItem)) continue; var changed = changedItem.Weapon(stain); - if (current.Value == changed.Value && state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + if (current.Value == changed.Value && state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) { _manager.ChangeItem(state, slot, currentItem, StateChanged.Source.Game); _manager.ChangeStain(state, slot, current.Stain, StateChanged.Source.Game); @@ -247,10 +278,10 @@ public class StateListener : IDisposable { case EquipSlot.MainHand: case EquipSlot.OffHand: - _applier.ChangeWeapon(objects, slot, currentItem, stain); + _applier.ChangeWeapon(objects, slot, currentItem, stain, crest); break; default: - _applier.ChangeArmor(objects, slot, current.ToArmor(), state[slot, false] is not StateChanged.Source.Ipc, + _applier.ChangeArmor(objects, slot, current.ToArmor(), crest, state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Ipc, state.ModelData.IsHatVisible()); break; } @@ -284,15 +315,20 @@ public class StateListener : IDisposable // Do nothing. But this usually can not happen because the hooked function also writes to game objects later. case UpdateState.Transformed: break; case UpdateState.Change: - if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + if (state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game); else apply = true; - if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + if (state[slot, ActorState.EquipField.Stain] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game); else apply = true; + + if (state[slot, ActorState.EquipField.Crest] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + _manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), StateChanged.Source.Game); + else + apply = true; break; case UpdateState.NoChange: apply = true; @@ -346,6 +382,7 @@ public class StateListener : IDisposable var item = _items.Identify(slot, actorArmor.Set, actorArmor.Variant); state.BaseData.SetItem(EquipSlot.Head, item); state.BaseData.SetStain(EquipSlot.Head, actorArmor.Stain); + state.BaseData.SetCrest(EquipSlot.Head, actor.GetCrest(slot)); return UpdateState.Change; } @@ -374,6 +411,20 @@ public class StateListener : IDisposable return change; } + private UpdateState UpdateBaseCrest(Actor actor, ActorState state, EquipSlot 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; + } + /// Handle a full equip slot update for base data and model data. private void HandleEquipSlot(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor) { @@ -383,12 +434,12 @@ public class StateListener : IDisposable // Update model state if not on fixed design. case UpdateState.Change: var apply = false; - if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + if (state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game); else apply = true; - if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + if (state[slot, ActorState.EquipField.Stain] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game); else apply = true; @@ -405,6 +456,24 @@ public class StateListener : IDisposable } } + private void HandleCrestVisibility(Actor actor, ActorState state, EquipSlot slot, ref bool visible) + { + switch (UpdateBaseCrest(actor, state, slot, visible)) + { + case UpdateState.Change: + if (state[slot, ActorState.EquipField.Crest] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + _manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), StateChanged.Source.Game); + else + visible = state.ModelData.Crest(slot); + break; + case UpdateState.NoChange: + case UpdateState.HatHack: + visible = state.ModelData.Crest(slot); + break; + case UpdateState.Transformed: break; + } + } + /// Update base data for a single changed weapon slot. private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon) { @@ -415,6 +484,7 @@ public class StateListener : IDisposable return UpdateState.Transformed; } + var crest = actor.GetCrest(slot); var baseData = state.BaseData.Weapon(slot); var change = UpdateState.NoChange; @@ -610,6 +680,7 @@ public class StateListener : IDisposable _penumbra.CreatingCharacterBase += OnCreatingCharacterBase; _penumbra.CreatedCharacterBase += OnCreatedCharacterBase; _slotUpdating.Subscribe(OnSlotUpdating, SlotUpdating.Priority.StateListener); + _crestVisibilityUpdating.Subscribe(OnCrestVisibilityUpdating, CrestVisibilityUpdating.Priority.StateListener); _movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener); _weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener); _visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener); @@ -623,6 +694,7 @@ public class StateListener : IDisposable _penumbra.CreatingCharacterBase -= OnCreatingCharacterBase; _penumbra.CreatedCharacterBase -= OnCreatedCharacterBase; _slotUpdating.Unsubscribe(OnSlotUpdating); + _crestVisibilityUpdating.Unsubscribe(OnCrestVisibilityUpdating); _movedEquipment.Unsubscribe(OnMovedEquipment); _weaponLoading.Unsubscribe(OnWeaponLoading); _visorState.Unsubscribe(OnVisorChange); diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 5157c60..0891769 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -143,10 +143,12 @@ public class StateManager : IReadOnlyDictionary ret.Customize = model.GetCustomize(); // We can not use the head slot data from the draw object if the hat is hidden. - var head = ret.IsHatVisible() || ignoreHatState ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); - var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant); + var head = ret.IsHatVisible() || ignoreHatState ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); + var headCrest = ret.IsHatVisible() || ignoreHatState ? model.GetCrest(EquipSlot.Head) : actor.GetCrest(EquipSlot.Head); + var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant); ret.SetItem(EquipSlot.Head, headItem); ret.SetStain(EquipSlot.Head, head.Stain); + ret.SetCrest(EquipSlot.Head, headCrest); // The other slots can be used from the draw object. foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) @@ -155,6 +157,7 @@ public class StateManager : IReadOnlyDictionary var item = _items.Identify(slot, armor.Set, armor.Variant); ret.SetItem(slot, item); ret.SetStain(slot, armor.Stain); + ret.SetCrest(slot, model.GetCrest(slot)); } // Weapons use the draw objects of the weapons, but require the game object either way. @@ -174,6 +177,7 @@ public class StateManager : IReadOnlyDictionary var item = _items.Identify(slot, armor.Set, armor.Variant); ret.SetItem(slot, item); ret.SetStain(slot, armor.Stain); + ret.SetCrest(slot, actor.GetCrest(slot)); } main = actor.GetMainhand(); @@ -187,8 +191,10 @@ public class StateManager : IReadOnlyDictionary var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, off.Variant, mainItem.Type); ret.SetItem(EquipSlot.MainHand, mainItem); ret.SetStain(EquipSlot.MainHand, main.Stain); + ret.SetCrest(EquipSlot.MainHand, actor.GetCrest(EquipSlot.MainHand)); ret.SetItem(EquipSlot.OffHand, offItem); ret.SetStain(EquipSlot.OffHand, off.Stain); + ret.SetCrest(EquipSlot.OffHand, actor.GetCrest(EquipSlot.OffHand)); // Wetness can technically only be set in GPose or via external tools. // It is only available in the game object. @@ -257,7 +263,7 @@ public class StateManager : IReadOnlyDictionary _event.Invoke(StateChanged.Type.EntireCustomize, source, state, actors, (old, applied)); } - /// Change a single piece of equipment without stain. + /// Change a single piece of equipment without stain or crest visibility. /// Do not use this in the same frame as ChangeStain, use instead. public void ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source, uint key = 0) { @@ -274,21 +280,25 @@ public class StateManager : IReadOnlyDictionary _event.Invoke(type, source, state, actors, (old, item, slot)); } - /// Change a single piece of equipment including stain. - public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source, uint key = 0) + /// Change a single piece of equipment including stain and crest visibility. + public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem? item, StainId? stain, bool? crest, StateChanged.Source source, uint key = 0) { - if (!_editor.ChangeEquip(state, slot, item, stain, source, out var old, out var oldStain, key)) + if (!_editor.ChangeEquip(state, slot, item, stain, crest, source, out var old, out var oldStain, out var oldCrest, key)) return; var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon; var actors = type is StateChanged.Type.Equip ? _applier.ChangeArmor(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc) : _applier.ChangeWeapon(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc, - item.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); + (item ?? old).Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); Glamourer.Log.Verbose( - $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}) and its stain from {oldStain.Id} to {stain.Id}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(type, source, state, actors, (old, item, slot)); - _event.Invoke(StateChanged.Type.Stain, source, state, actors, (oldStain, stain, slot)); + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {(item ?? old).Name} ({(item ?? old).ItemId}), its stain from {oldStain.Id} to {(stain ?? oldStain).Id} and its crest visibility from {oldCrest} to {crest ?? oldCrest}. [Affecting {actors.ToLazyString("nothing")}.]"); + if (item.HasValue) + _event.Invoke(type, source, state, actors, (old, item.Value, slot)); + if (stain.HasValue) + _event.Invoke(StateChanged.Type.Stain, source, state, actors, (oldStain, stain.Value, slot)); + if (crest.HasValue) + _event.Invoke(StateChanged.Type.Crest, source, state, actors, (oldCrest, crest.Value, slot)); } /// Change only the stain of an equipment piece. @@ -304,6 +314,19 @@ public class StateManager : IReadOnlyDictionary _event.Invoke(StateChanged.Type.Stain, source, state, actors, (old, stain, slot)); } + /// Change only the crest visibility of an equipment piece. + /// Do not use this in the same frame as ChangeEquip, use instead. + public void ChangeCrest(ActorState state, EquipSlot slot, bool crest, StateChanged.Source source, uint key = 0) + { + if (!_editor.ChangeCrest(state, slot, crest, source, out var old, key)) + return; + + var actors = _applier.ChangeCrest(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} crest visibility 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,15 +379,16 @@ 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) + void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain, bool applyCrest) { - var unused = (applyPiece, applyStain) switch + var unused = (applyPiece, applyStain, applyCrest) 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), + (false, false, false) => false, + (true, false, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key), + (false, true, false) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key), + (false, false, true) => _editor.ChangeCrest(state, slot, design.DesignData.Crest(slot), source, out _, key), + _ => _editor.ChangeEquip(state, slot, applyPiece ? design.DesignData.Item(slot) : null, applyStain ? design.DesignData.Stain(slot) : null, applyCrest ? design.DesignData.Crest(slot) : null, source, out _, + out _, out _, key), }; } @@ -392,7 +416,7 @@ public class StateManager : IReadOnlyDictionary redraw |= applied.RequiresRedraw(); foreach (var slot in EquipSlotExtensions.FullSlots) - HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot)); + HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot), design.DoApplyCrest(slot)); } var actors = ApplyAll(state, redraw, false); @@ -415,14 +439,14 @@ public class StateManager : IReadOnlyDictionary _applier.ChangeCustomize(actors, state.ModelData.Customize); foreach (var slot in EquipSlotExtensions.EqdpSlots) { - _applier.ChangeArmor(actors, slot, state.ModelData.Armor(slot), state[slot, false] is not StateChanged.Source.Ipc, + _applier.ChangeArmor(actors, slot, state.ModelData.Armor(slot), state.ModelData.Crest(slot), state[slot, ActorState.EquipField.Item] is not StateChanged.Source.Ipc, state.ModelData.IsHatVisible()); } var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors; - _applier.ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); + _applier.ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand), state.ModelData.Crest(EquipSlot.MainHand)); var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors; - _applier.ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand)); + _applier.ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand), state.ModelData.Crest(EquipSlot.OffHand)); } if (state.ModelData.IsHuman) @@ -450,8 +474,9 @@ public class StateManager : IReadOnlyDictionary foreach (var slot in EquipSlotExtensions.FullSlots) { - state[slot, true] = StateChanged.Source.Game; - state[slot, false] = StateChanged.Source.Game; + state[slot, ActorState.EquipField.Stain] = StateChanged.Source.Game; + state[slot, ActorState.EquipField.Item] = StateChanged.Source.Game; + state[slot, ActorState.EquipField.Crest] = StateChanged.Source.Game; } foreach (var type in Enum.GetValues()) @@ -478,15 +503,21 @@ public class StateManager : IReadOnlyDictionary foreach (var slot in EquipSlotExtensions.FullSlots) { - if (state[slot, true] is StateChanged.Source.Fixed) + if (state[slot, ActorState.EquipField.Crest] is StateChanged.Source.Fixed) { - state[slot, true] = StateChanged.Source.Game; + state[slot, ActorState.EquipField.Crest] = StateChanged.Source.Game; + state.ModelData.SetCrest(slot, state.BaseData.Crest(slot)); + } + + if (state[slot, ActorState.EquipField.Stain] is StateChanged.Source.Fixed) + { + state[slot, ActorState.EquipField.Stain] = StateChanged.Source.Game; state.ModelData.SetStain(slot, state.BaseData.Stain(slot)); } - if (state[slot, false] is StateChanged.Source.Fixed) + if (state[slot, ActorState.EquipField.Item] is StateChanged.Source.Fixed) { - state[slot, false] = StateChanged.Source.Game; + state[slot, ActorState.EquipField.Item] = StateChanged.Source.Game; state.ModelData.SetItem(slot, state.BaseData.Item(slot)); } }